mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-07-09 02:26:47 +00:00
clean up lazy readonly collections and add tests
This commit is contained in:
@@ -10,23 +10,23 @@ namespace SharpCompress;
|
||||
internal sealed class LazyAsyncReadOnlyCollection<T>(IAsyncEnumerable<T> source)
|
||||
: IAsyncEnumerable<T>
|
||||
{
|
||||
private readonly List<T> backing = new();
|
||||
private readonly IAsyncEnumerator<T> source = source.GetAsyncEnumerator();
|
||||
private bool fullyLoaded;
|
||||
private readonly List<T> _backing = new();
|
||||
private readonly IAsyncEnumerator<T> _source = source.GetAsyncEnumerator();
|
||||
private bool _fullyLoaded;
|
||||
|
||||
private class LazyLoader(
|
||||
LazyAsyncReadOnlyCollection<T> lazyReadOnlyCollection,
|
||||
CancellationToken cancellationToken
|
||||
) : IAsyncEnumerator<T>
|
||||
{
|
||||
private bool disposed;
|
||||
private int index = -1;
|
||||
private bool _disposed;
|
||||
private int _index = -1;
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
if (!disposed)
|
||||
if (!_disposed)
|
||||
{
|
||||
disposed = true;
|
||||
_disposed = true;
|
||||
}
|
||||
return default;
|
||||
}
|
||||
@@ -34,27 +34,27 @@ internal sealed class LazyAsyncReadOnlyCollection<T>(IAsyncEnumerable<T> source)
|
||||
public async ValueTask<bool> MoveNextAsync()
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (index + 1 < lazyReadOnlyCollection.backing.Count)
|
||||
if (_index + 1 < lazyReadOnlyCollection._backing.Count)
|
||||
{
|
||||
index++;
|
||||
_index++;
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
!lazyReadOnlyCollection.fullyLoaded
|
||||
&& await lazyReadOnlyCollection.source.MoveNextAsync()
|
||||
!lazyReadOnlyCollection._fullyLoaded
|
||||
&& await lazyReadOnlyCollection._source.MoveNextAsync()
|
||||
)
|
||||
{
|
||||
lazyReadOnlyCollection.backing.Add(lazyReadOnlyCollection.source.Current);
|
||||
index++;
|
||||
lazyReadOnlyCollection._backing.Add(lazyReadOnlyCollection._source.Current);
|
||||
_index++;
|
||||
return true;
|
||||
}
|
||||
lazyReadOnlyCollection.fullyLoaded = true;
|
||||
lazyReadOnlyCollection._fullyLoaded = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
#region IEnumerator<T> Members
|
||||
|
||||
public T Current => lazyReadOnlyCollection.backing[index];
|
||||
public T Current => lazyReadOnlyCollection._backing[_index];
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -62,9 +62,9 @@ internal sealed class LazyAsyncReadOnlyCollection<T>(IAsyncEnumerable<T> source)
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!disposed)
|
||||
if (!_disposed)
|
||||
{
|
||||
disposed = true;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,18 +73,18 @@ internal sealed class LazyAsyncReadOnlyCollection<T>(IAsyncEnumerable<T> source)
|
||||
|
||||
internal async ValueTask EnsureFullyLoaded()
|
||||
{
|
||||
if (!fullyLoaded)
|
||||
if (!_fullyLoaded)
|
||||
{
|
||||
var loader = new LazyLoader(this, CancellationToken.None);
|
||||
while (await loader.MoveNextAsync())
|
||||
{
|
||||
// Intentionally empty
|
||||
}
|
||||
fullyLoaded = true;
|
||||
_fullyLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal IEnumerable<T> GetLoaded() => backing;
|
||||
internal IEnumerable<T> GetLoaded() => _backing;
|
||||
|
||||
#region ICollection<T> Members
|
||||
|
||||
|
||||
@@ -8,24 +8,24 @@ namespace SharpCompress;
|
||||
|
||||
internal sealed class LazyReadOnlyCollection<T> : ICollection<T>
|
||||
{
|
||||
private readonly List<T> backing = new();
|
||||
private readonly IEnumerator<T> source;
|
||||
private bool fullyLoaded;
|
||||
private readonly List<T> _backing = new();
|
||||
private readonly IEnumerator<T> _source;
|
||||
private bool _fullyLoaded;
|
||||
|
||||
public LazyReadOnlyCollection(IEnumerable<T> source) => this.source = source.GetEnumerator();
|
||||
public LazyReadOnlyCollection(IEnumerable<T> source) => _source = source.GetEnumerator();
|
||||
|
||||
private class LazyLoader : IEnumerator<T>
|
||||
{
|
||||
private readonly LazyReadOnlyCollection<T> lazyReadOnlyCollection;
|
||||
private bool disposed;
|
||||
private int index = -1;
|
||||
private readonly LazyReadOnlyCollection<T> _lazyReadOnlyCollection;
|
||||
private bool _disposed;
|
||||
private int _index = -1;
|
||||
|
||||
internal LazyLoader(LazyReadOnlyCollection<T> lazyReadOnlyCollection) =>
|
||||
this.lazyReadOnlyCollection = lazyReadOnlyCollection;
|
||||
_lazyReadOnlyCollection = lazyReadOnlyCollection;
|
||||
|
||||
#region IEnumerator<T> Members
|
||||
|
||||
public T Current => lazyReadOnlyCollection.backing[index];
|
||||
public T Current => _lazyReadOnlyCollection._backing[_index];
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -33,9 +33,9 @@ internal sealed class LazyReadOnlyCollection<T> : ICollection<T>
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!disposed)
|
||||
if (!_disposed)
|
||||
{
|
||||
disposed = true;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,18 +47,18 @@ internal sealed class LazyReadOnlyCollection<T> : ICollection<T>
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (index + 1 < lazyReadOnlyCollection.backing.Count)
|
||||
if (_index + 1 < _lazyReadOnlyCollection._backing.Count)
|
||||
{
|
||||
index++;
|
||||
_index++;
|
||||
return true;
|
||||
}
|
||||
if (!lazyReadOnlyCollection.fullyLoaded && lazyReadOnlyCollection.source.MoveNext())
|
||||
if (!_lazyReadOnlyCollection._fullyLoaded && _lazyReadOnlyCollection._source.MoveNext())
|
||||
{
|
||||
lazyReadOnlyCollection.backing.Add(lazyReadOnlyCollection.source.Current);
|
||||
index++;
|
||||
_lazyReadOnlyCollection._backing.Add(_lazyReadOnlyCollection._source.Current);
|
||||
_index++;
|
||||
return true;
|
||||
}
|
||||
lazyReadOnlyCollection.fullyLoaded = true;
|
||||
_lazyReadOnlyCollection._fullyLoaded = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -69,14 +69,14 @@ internal sealed class LazyReadOnlyCollection<T> : ICollection<T>
|
||||
|
||||
internal void EnsureFullyLoaded()
|
||||
{
|
||||
if (!fullyLoaded)
|
||||
if (!_fullyLoaded)
|
||||
{
|
||||
this.ForEach(x => { });
|
||||
fullyLoaded = true;
|
||||
_fullyLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal IEnumerable<T> GetLoaded() => backing;
|
||||
internal IEnumerable<T> GetLoaded() => _backing;
|
||||
|
||||
#region ICollection<T> Members
|
||||
|
||||
@@ -87,13 +87,13 @@ internal sealed class LazyReadOnlyCollection<T> : ICollection<T>
|
||||
public bool Contains(T item)
|
||||
{
|
||||
EnsureFullyLoaded();
|
||||
return backing.Contains(item);
|
||||
return _backing.Contains(item);
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
EnsureFullyLoaded();
|
||||
backing.CopyTo(array, arrayIndex);
|
||||
_backing.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public int Count
|
||||
@@ -101,7 +101,7 @@ internal sealed class LazyReadOnlyCollection<T> : ICollection<T>
|
||||
get
|
||||
{
|
||||
EnsureFullyLoaded();
|
||||
return backing.Count;
|
||||
return _backing.Count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
358
tests/SharpCompress.Test/LazyAsyncReadOnlyCollectionTests.cs
Normal file
358
tests/SharpCompress.Test/LazyAsyncReadOnlyCollectionTests.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test;
|
||||
|
||||
public class LazyAsyncReadOnlyCollectionTests
|
||||
{
|
||||
// Helper class to track how many times items are enumerated from the source
|
||||
private class TrackingAsyncEnumerable<T> : IAsyncEnumerable<T>
|
||||
{
|
||||
private readonly List<T> _items;
|
||||
public int EnumerationCount { get; private set; }
|
||||
public int ItemsRequestedCount { get; private set; }
|
||||
|
||||
public TrackingAsyncEnumerable(params T[] items)
|
||||
{
|
||||
_items = new List<T>(items);
|
||||
}
|
||||
|
||||
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnumerationCount++;
|
||||
return new TrackingEnumerator(this, cancellationToken);
|
||||
}
|
||||
|
||||
private class TrackingEnumerator : IAsyncEnumerator<T>
|
||||
{
|
||||
private readonly TrackingAsyncEnumerable<T> _parent;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
private int _index = -1;
|
||||
|
||||
public TrackingEnumerator(
|
||||
TrackingAsyncEnumerable<T> parent,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
_parent = parent;
|
||||
_cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public T Current => _parent._items[_index];
|
||||
|
||||
public async ValueTask<bool> MoveNextAsync()
|
||||
{
|
||||
_cancellationToken.ThrowIfCancellationRequested();
|
||||
await Task.Yield(); // Simulate async behavior
|
||||
_index++;
|
||||
if (_index < _parent._items.Count)
|
||||
{
|
||||
_parent.ItemsRequestedCount++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => default;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BasicEnumeration_IteratesThroughAllItems()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingAsyncEnumerable<int>(1, 2, 3, 4, 5);
|
||||
var collection = new LazyAsyncReadOnlyCollection<int>(source);
|
||||
|
||||
// Act
|
||||
var results = new List<int>();
|
||||
await foreach (var item in collection)
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, results.Count);
|
||||
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, results);
|
||||
Assert.Equal(1, source.EnumerationCount);
|
||||
Assert.Equal(5, source.ItemsRequestedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MultipleEnumerations_UsesCachedBackingList()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingAsyncEnumerable<string>("a", "b", "c");
|
||||
var collection = new LazyAsyncReadOnlyCollection<string>(source);
|
||||
|
||||
// Act - First enumeration
|
||||
var firstResults = new List<string>();
|
||||
await foreach (var item in collection)
|
||||
{
|
||||
firstResults.Add(item);
|
||||
}
|
||||
|
||||
var itemsRequestedAfterFirst = source.ItemsRequestedCount;
|
||||
|
||||
// Act - Second enumeration
|
||||
var secondResults = new List<string>();
|
||||
await foreach (var item in collection)
|
||||
{
|
||||
secondResults.Add(item);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(firstResults, secondResults);
|
||||
Assert.Equal(new[] { "a", "b", "c" }, secondResults);
|
||||
|
||||
// Source should only be enumerated once
|
||||
Assert.Equal(1, source.EnumerationCount);
|
||||
|
||||
// Items should only be requested from source during first enumeration
|
||||
Assert.Equal(itemsRequestedAfterFirst, source.ItemsRequestedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureFullyLoaded_LoadsAllItemsIntoBackingList()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingAsyncEnumerable<int>(10, 20, 30, 40);
|
||||
var collection = new LazyAsyncReadOnlyCollection<int>(source);
|
||||
|
||||
// Act
|
||||
await collection.EnsureFullyLoaded();
|
||||
var loaded = collection.GetLoaded().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, loaded.Count);
|
||||
Assert.Equal(new[] { 10, 20, 30, 40 }, loaded);
|
||||
Assert.Equal(4, source.ItemsRequestedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLoaded_ReturnsOnlyLoadedItemsBeforeFullEnumeration()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingAsyncEnumerable<int>(1, 2, 3, 4, 5);
|
||||
var collection = new LazyAsyncReadOnlyCollection<int>(source);
|
||||
|
||||
// Act - Partially enumerate (only first 2 items)
|
||||
var enumerator = collection.GetAsyncEnumerator();
|
||||
await enumerator.MoveNextAsync(); // Load item 1
|
||||
await enumerator.MoveNextAsync(); // Load item 2
|
||||
|
||||
var loadedItems = collection.GetLoaded().ToList();
|
||||
|
||||
// Continue enumeration
|
||||
await enumerator.MoveNextAsync(); // Load item 3
|
||||
var loadedItemsAfter = collection.GetLoaded().ToList();
|
||||
|
||||
await enumerator.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, loadedItems.Count);
|
||||
Assert.Equal(new[] { 1, 2 }, loadedItems);
|
||||
|
||||
Assert.Equal(3, loadedItemsAfter.Count);
|
||||
Assert.Equal(new[] { 1, 2, 3 }, loadedItemsAfter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancellationToken_PassedToGetAsyncEnumerator_HonorsToken()
|
||||
{
|
||||
// Arrange
|
||||
var cts = new CancellationTokenSource();
|
||||
var source = new TrackingAsyncEnumerable<int>(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
|
||||
var collection = new LazyAsyncReadOnlyCollection<int>(source);
|
||||
|
||||
// Act & Assert
|
||||
var results = new List<int>();
|
||||
var exception = await Assert.ThrowsAsync<OperationCanceledException>(async () =>
|
||||
{
|
||||
await foreach (var item in collection.WithCancellation(cts.Token))
|
||||
{
|
||||
results.Add(item);
|
||||
if (item == 3)
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal(3, results.Count);
|
||||
Assert.Equal(new[] { 1, 2, 3 }, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancellationDuringMoveNextAsync_ThrowsOperationCanceledException()
|
||||
{
|
||||
// Arrange
|
||||
var cts = new CancellationTokenSource();
|
||||
var source = CreateDelayedAsyncEnumerable(
|
||||
new[] { 1, 2, 3, 4, 5 },
|
||||
TimeSpan.FromMilliseconds(50)
|
||||
);
|
||||
var collection = new LazyAsyncReadOnlyCollection<int>(source);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
|
||||
{
|
||||
var enumerator = collection.GetAsyncEnumerator(cts.Token);
|
||||
await enumerator.MoveNextAsync();
|
||||
await enumerator.MoveNextAsync();
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
await enumerator.MoveNextAsync(); // Should throw
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmptySourceEnumerable_ReturnsNoItems()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingAsyncEnumerable<int>();
|
||||
var collection = new LazyAsyncReadOnlyCollection<int>(source);
|
||||
|
||||
// Act
|
||||
var results = new List<int>();
|
||||
await foreach (var item in collection)
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
Assert.Equal(1, source.EnumerationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SingleItemSourceEnumerable_ReturnsSingleItem()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingAsyncEnumerable<string>("only");
|
||||
var collection = new LazyAsyncReadOnlyCollection<string>(source);
|
||||
|
||||
// Act
|
||||
var results = new List<string>();
|
||||
await foreach (var item in collection)
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Equal("only", results[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialEnumeration_ThenGetLoaded_ReturnsOnlyEnumeratedItems()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingAsyncEnumerable<int>(10, 20, 30, 40, 50);
|
||||
var collection = new LazyAsyncReadOnlyCollection<int>(source);
|
||||
|
||||
// Act - Enumerate only first 3 items
|
||||
await using (var enumerator = collection.GetAsyncEnumerator())
|
||||
{
|
||||
var hasMore = await enumerator.MoveNextAsync();
|
||||
Assert.True(hasMore);
|
||||
hasMore = await enumerator.MoveNextAsync();
|
||||
Assert.True(hasMore);
|
||||
hasMore = await enumerator.MoveNextAsync();
|
||||
Assert.True(hasMore);
|
||||
}
|
||||
|
||||
var loadedItems = collection.GetLoaded().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, loadedItems.Count);
|
||||
Assert.Equal(new[] { 10, 20, 30 }, loadedItems);
|
||||
Assert.Equal(3, source.ItemsRequestedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentEnumerations_ShareBackingList()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingAsyncEnumerable<int>(1, 2, 3, 4, 5);
|
||||
var collection = new LazyAsyncReadOnlyCollection<int>(source);
|
||||
|
||||
// Act - Fully load the collection first, then enumerate from two threads
|
||||
await collection.EnsureFullyLoaded();
|
||||
|
||||
var task1 = Task.Run(async () =>
|
||||
{
|
||||
var results = new List<int>();
|
||||
await foreach (var item in collection)
|
||||
{
|
||||
results.Add(item);
|
||||
await Task.Delay(5);
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
var task2 = Task.Run(async () =>
|
||||
{
|
||||
var results = new List<int>();
|
||||
await foreach (var item in collection)
|
||||
{
|
||||
results.Add(item);
|
||||
await Task.Delay(5);
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
var results1 = await task1;
|
||||
var results2 = await task2;
|
||||
|
||||
// Assert - Both enumerations should see all items from the shared backing list
|
||||
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, results1);
|
||||
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, results2);
|
||||
Assert.Equal(5, source.ItemsRequestedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_OnLazyLoader_CompletesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingAsyncEnumerable<int>(1, 2, 3);
|
||||
var collection = new LazyAsyncReadOnlyCollection<int>(source);
|
||||
|
||||
// Act
|
||||
await using (var enumerator = collection.GetAsyncEnumerator())
|
||||
{
|
||||
await enumerator.MoveNextAsync();
|
||||
var firstItem = enumerator.Current;
|
||||
Assert.Equal(1, firstItem);
|
||||
|
||||
// Dispose is called automatically by await using
|
||||
}
|
||||
|
||||
// Assert - should be able to enumerate again after disposal
|
||||
var results = new List<int>();
|
||||
await foreach (var item in collection)
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
|
||||
Assert.Equal(new[] { 1, 2, 3 }, results);
|
||||
}
|
||||
|
||||
// Helper method to create an async enumerable with delays
|
||||
private static async IAsyncEnumerable<T> CreateDelayedAsyncEnumerable<T>(
|
||||
IEnumerable<T> items,
|
||||
TimeSpan delay
|
||||
)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
await Task.Delay(delay);
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
382
tests/SharpCompress.Test/LazyReadOnlyCollectionTests.cs
Normal file
382
tests/SharpCompress.Test/LazyReadOnlyCollectionTests.cs
Normal file
@@ -0,0 +1,382 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test;
|
||||
|
||||
public class LazyReadOnlyCollectionTests
|
||||
{
|
||||
// Helper class to track how many times items are enumerated from the source
|
||||
private class TrackingEnumerable<T> : IEnumerable<T>
|
||||
{
|
||||
private readonly List<T> _items;
|
||||
public int EnumerationCount { get; private set; }
|
||||
public int ItemsRequestedCount { get; private set; }
|
||||
|
||||
public TrackingEnumerable(params T[] items)
|
||||
{
|
||||
_items = new List<T>(items);
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
EnumerationCount++;
|
||||
return new TrackingEnumerator(this);
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>
|
||||
GetEnumerator();
|
||||
|
||||
private class TrackingEnumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly TrackingEnumerable<T> _parent;
|
||||
private int _index = -1;
|
||||
|
||||
public TrackingEnumerator(TrackingEnumerable<T> parent)
|
||||
{
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public T Current => _parent._items[_index];
|
||||
|
||||
object? System.Collections.IEnumerator.Current => Current;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_index++;
|
||||
if (_index < _parent._items.Count)
|
||||
{
|
||||
_parent.ItemsRequestedCount++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Reset() => throw new NotSupportedException();
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicEnumeration_IteratesThroughAllItems()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(1, 2, 3, 4, 5);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act
|
||||
var results = new List<int>();
|
||||
foreach (var item in collection)
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, results.Count);
|
||||
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, results);
|
||||
Assert.Equal(1, source.EnumerationCount);
|
||||
Assert.Equal(5, source.ItemsRequestedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleEnumerations_UsesCachedBackingList()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<string>("a", "b", "c");
|
||||
var collection = new LazyReadOnlyCollection<string>(source);
|
||||
|
||||
// Act - First enumeration
|
||||
var firstResults = new List<string>();
|
||||
foreach (var item in collection)
|
||||
{
|
||||
firstResults.Add(item);
|
||||
}
|
||||
|
||||
var itemsRequestedAfterFirst = source.ItemsRequestedCount;
|
||||
|
||||
// Act - Second enumeration
|
||||
var secondResults = new List<string>();
|
||||
foreach (var item in collection)
|
||||
{
|
||||
secondResults.Add(item);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(firstResults, secondResults);
|
||||
Assert.Equal(new[] { "a", "b", "c" }, secondResults);
|
||||
|
||||
// Source should only be enumerated once
|
||||
Assert.Equal(1, source.EnumerationCount);
|
||||
|
||||
// Items should only be requested from source during first enumeration
|
||||
Assert.Equal(itemsRequestedAfterFirst, source.ItemsRequestedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureFullyLoaded_LoadsAllItemsIntoBackingList()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(10, 20, 30, 40);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act
|
||||
collection.EnsureFullyLoaded();
|
||||
var loaded = collection.GetLoaded().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, loaded.Count);
|
||||
Assert.Equal(new[] { 10, 20, 30, 40 }, loaded);
|
||||
Assert.Equal(4, source.ItemsRequestedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLoaded_ReturnsOnlyLoadedItemsBeforeFullEnumeration()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(1, 2, 3, 4, 5);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act - Partially enumerate (only first 2 items)
|
||||
using var enumerator = collection.GetEnumerator();
|
||||
enumerator.MoveNext(); // Load item 1
|
||||
enumerator.MoveNext(); // Load item 2
|
||||
|
||||
var loadedItems = collection.GetLoaded().ToList();
|
||||
|
||||
// Continue enumeration
|
||||
enumerator.MoveNext(); // Load item 3
|
||||
var loadedItemsAfter = collection.GetLoaded().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, loadedItems.Count);
|
||||
Assert.Equal(new[] { 1, 2 }, loadedItems);
|
||||
|
||||
Assert.Equal(3, loadedItemsAfter.Count);
|
||||
Assert.Equal(new[] { 1, 2, 3 }, loadedItemsAfter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Count_TriggersFullLoad()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(1, 2, 3, 4, 5);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act
|
||||
var count = collection.Count;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, count);
|
||||
Assert.Equal(5, source.ItemsRequestedCount);
|
||||
Assert.Equal(5, collection.GetLoaded().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_TriggersFullLoadAndSearches()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<string>("apple", "banana", "cherry");
|
||||
var collection = new LazyReadOnlyCollection<string>(source);
|
||||
|
||||
// Act
|
||||
var containsBanana = collection.Contains("banana");
|
||||
var containsOrange = collection.Contains("orange");
|
||||
|
||||
// Assert
|
||||
Assert.True(containsBanana);
|
||||
Assert.False(containsOrange);
|
||||
Assert.Equal(3, source.ItemsRequestedCount);
|
||||
Assert.Equal(3, collection.GetLoaded().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_TriggersFullLoadAndCopiesArray()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(10, 20, 30);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
var array = new int[5];
|
||||
|
||||
// Act
|
||||
collection.CopyTo(array, 1);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, array[0]);
|
||||
Assert.Equal(10, array[1]);
|
||||
Assert.Equal(20, array[2]);
|
||||
Assert.Equal(30, array[3]);
|
||||
Assert.Equal(0, array[4]);
|
||||
Assert.Equal(3, source.ItemsRequestedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptySourceEnumerable_ReturnsNoItems()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>();
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act
|
||||
var results = new List<int>();
|
||||
foreach (var item in collection)
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Empty(results);
|
||||
Assert.Equal(0, collection.Count);
|
||||
Assert.Equal(1, source.EnumerationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleItemSourceEnumerable_ReturnsSingleItem()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<string>("only");
|
||||
var collection = new LazyReadOnlyCollection<string>(source);
|
||||
|
||||
// Act
|
||||
var results = new List<string>();
|
||||
foreach (var item in collection)
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Equal("only", results[0]);
|
||||
Assert.Equal(1, collection.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartialEnumeration_ThenGetLoaded_ReturnsOnlyEnumeratedItems()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(10, 20, 30, 40, 50);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act - Enumerate only first 3 items
|
||||
using (var enumerator = collection.GetEnumerator())
|
||||
{
|
||||
var hasMore = enumerator.MoveNext();
|
||||
Assert.True(hasMore);
|
||||
hasMore = enumerator.MoveNext();
|
||||
Assert.True(hasMore);
|
||||
hasMore = enumerator.MoveNext();
|
||||
Assert.True(hasMore);
|
||||
}
|
||||
|
||||
var loadedItems = collection.GetLoaded().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, loadedItems.Count);
|
||||
Assert.Equal(new[] { 10, 20, 30 }, loadedItems);
|
||||
Assert.Equal(3, source.ItemsRequestedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ThrowsNotSupportedException()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(1, 2, 3);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act & Assert
|
||||
using var enumerator = collection.GetEnumerator();
|
||||
enumerator.MoveNext();
|
||||
Assert.Throws<NotSupportedException>(() => enumerator.Reset());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_OnEnumerator_CompletesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(1, 2, 3);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act
|
||||
using (var enumerator = collection.GetEnumerator())
|
||||
{
|
||||
enumerator.MoveNext();
|
||||
var firstItem = enumerator.Current;
|
||||
Assert.Equal(1, firstItem);
|
||||
|
||||
// Dispose is called automatically by using
|
||||
}
|
||||
|
||||
// Assert - should be able to enumerate again after disposal
|
||||
var results = new List<int>();
|
||||
foreach (var item in collection)
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
|
||||
Assert.Equal(new[] { 1, 2, 3 }, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsReadOnly_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(1, 2, 3);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(collection.IsReadOnly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_ThrowsNotSupportedException()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(1, 2, 3);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotSupportedException>(() => collection.Add(4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_ThrowsNotSupportedException()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(1, 2, 3);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotSupportedException>(() => collection.Clear());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_ThrowsNotSupportedException()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(1, 2, 3);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<NotSupportedException>(() => collection.Remove(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonGenericEnumerator_WorksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var source = new TrackingEnumerable<int>(1, 2, 3);
|
||||
var collection = new LazyReadOnlyCollection<int>(source);
|
||||
|
||||
// Act - Use non-generic IEnumerator
|
||||
var results = new List<int>();
|
||||
var enumerable = (System.Collections.IEnumerable)collection;
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
results.Add((int)item);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[] { 1, 2, 3 }, results);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user