From ca4cf25a1f07c54dfdcae8444dd424408fa4eace Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 13 Jan 2026 16:39:55 +0000 Subject: [PATCH] clean up lazy readonly collections and add tests --- .../LazyAsyncReadOnlyCollection.cs | 40 +- src/SharpCompress/LazyReadOnlyCollection.cs | 46 +-- .../LazyAsyncReadOnlyCollectionTests.cs | 358 ++++++++++++++++ .../LazyReadOnlyCollectionTests.cs | 382 ++++++++++++++++++ 4 files changed, 783 insertions(+), 43 deletions(-) create mode 100644 tests/SharpCompress.Test/LazyAsyncReadOnlyCollectionTests.cs create mode 100644 tests/SharpCompress.Test/LazyReadOnlyCollectionTests.cs diff --git a/src/SharpCompress/LazyAsyncReadOnlyCollection.cs b/src/SharpCompress/LazyAsyncReadOnlyCollection.cs index 85caf611..114bc71a 100644 --- a/src/SharpCompress/LazyAsyncReadOnlyCollection.cs +++ b/src/SharpCompress/LazyAsyncReadOnlyCollection.cs @@ -10,23 +10,23 @@ namespace SharpCompress; internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable source) : IAsyncEnumerable { - private readonly List backing = new(); - private readonly IAsyncEnumerator source = source.GetAsyncEnumerator(); - private bool fullyLoaded; + private readonly List _backing = new(); + private readonly IAsyncEnumerator _source = source.GetAsyncEnumerator(); + private bool _fullyLoaded; private class LazyLoader( LazyAsyncReadOnlyCollection lazyReadOnlyCollection, CancellationToken cancellationToken ) : IAsyncEnumerator { - 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(IAsyncEnumerable source) public async ValueTask 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 Members - public T Current => lazyReadOnlyCollection.backing[index]; + public T Current => lazyReadOnlyCollection._backing[_index]; #endregion @@ -62,9 +62,9 @@ internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable source) public void Dispose() { - if (!disposed) + if (!_disposed) { - disposed = true; + _disposed = true; } } @@ -73,18 +73,18 @@ internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable 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 GetLoaded() => backing; + internal IEnumerable GetLoaded() => _backing; #region ICollection Members diff --git a/src/SharpCompress/LazyReadOnlyCollection.cs b/src/SharpCompress/LazyReadOnlyCollection.cs index cc9cb3fd..eee60bc7 100644 --- a/src/SharpCompress/LazyReadOnlyCollection.cs +++ b/src/SharpCompress/LazyReadOnlyCollection.cs @@ -8,24 +8,24 @@ namespace SharpCompress; internal sealed class LazyReadOnlyCollection : ICollection { - private readonly List backing = new(); - private readonly IEnumerator source; - private bool fullyLoaded; + private readonly List _backing = new(); + private readonly IEnumerator _source; + private bool _fullyLoaded; - public LazyReadOnlyCollection(IEnumerable source) => this.source = source.GetEnumerator(); + public LazyReadOnlyCollection(IEnumerable source) => _source = source.GetEnumerator(); private class LazyLoader : IEnumerator { - private readonly LazyReadOnlyCollection lazyReadOnlyCollection; - private bool disposed; - private int index = -1; + private readonly LazyReadOnlyCollection _lazyReadOnlyCollection; + private bool _disposed; + private int _index = -1; internal LazyLoader(LazyReadOnlyCollection lazyReadOnlyCollection) => - this.lazyReadOnlyCollection = lazyReadOnlyCollection; + _lazyReadOnlyCollection = lazyReadOnlyCollection; #region IEnumerator Members - public T Current => lazyReadOnlyCollection.backing[index]; + public T Current => _lazyReadOnlyCollection._backing[_index]; #endregion @@ -33,9 +33,9 @@ internal sealed class LazyReadOnlyCollection : ICollection public void Dispose() { - if (!disposed) + if (!_disposed) { - disposed = true; + _disposed = true; } } @@ -47,18 +47,18 @@ internal sealed class LazyReadOnlyCollection : ICollection 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 : ICollection internal void EnsureFullyLoaded() { - if (!fullyLoaded) + if (!_fullyLoaded) { this.ForEach(x => { }); - fullyLoaded = true; + _fullyLoaded = true; } } - internal IEnumerable GetLoaded() => backing; + internal IEnumerable GetLoaded() => _backing; #region ICollection Members @@ -87,13 +87,13 @@ internal sealed class LazyReadOnlyCollection : ICollection 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 : ICollection get { EnsureFullyLoaded(); - return backing.Count; + return _backing.Count; } } diff --git a/tests/SharpCompress.Test/LazyAsyncReadOnlyCollectionTests.cs b/tests/SharpCompress.Test/LazyAsyncReadOnlyCollectionTests.cs new file mode 100644 index 00000000..aa9c2af4 --- /dev/null +++ b/tests/SharpCompress.Test/LazyAsyncReadOnlyCollectionTests.cs @@ -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 : IAsyncEnumerable + { + private readonly List _items; + public int EnumerationCount { get; private set; } + public int ItemsRequestedCount { get; private set; } + + public TrackingAsyncEnumerable(params T[] items) + { + _items = new List(items); + } + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + EnumerationCount++; + return new TrackingEnumerator(this, cancellationToken); + } + + private class TrackingEnumerator : IAsyncEnumerator + { + private readonly TrackingAsyncEnumerable _parent; + private readonly CancellationToken _cancellationToken; + private int _index = -1; + + public TrackingEnumerator( + TrackingAsyncEnumerable parent, + CancellationToken cancellationToken + ) + { + _parent = parent; + _cancellationToken = cancellationToken; + } + + public T Current => _parent._items[_index]; + + public async ValueTask 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(1, 2, 3, 4, 5); + var collection = new LazyAsyncReadOnlyCollection(source); + + // Act + var results = new List(); + 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("a", "b", "c"); + var collection = new LazyAsyncReadOnlyCollection(source); + + // Act - First enumeration + var firstResults = new List(); + await foreach (var item in collection) + { + firstResults.Add(item); + } + + var itemsRequestedAfterFirst = source.ItemsRequestedCount; + + // Act - Second enumeration + var secondResults = new List(); + 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(10, 20, 30, 40); + var collection = new LazyAsyncReadOnlyCollection(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(1, 2, 3, 4, 5); + var collection = new LazyAsyncReadOnlyCollection(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(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + var collection = new LazyAsyncReadOnlyCollection(source); + + // Act & Assert + var results = new List(); + var exception = await Assert.ThrowsAsync(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(source); + + // Act & Assert + await Assert.ThrowsAsync(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(); + var collection = new LazyAsyncReadOnlyCollection(source); + + // Act + var results = new List(); + 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("only"); + var collection = new LazyAsyncReadOnlyCollection(source); + + // Act + var results = new List(); + 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(10, 20, 30, 40, 50); + var collection = new LazyAsyncReadOnlyCollection(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(1, 2, 3, 4, 5); + var collection = new LazyAsyncReadOnlyCollection(source); + + // Act - Fully load the collection first, then enumerate from two threads + await collection.EnsureFullyLoaded(); + + var task1 = Task.Run(async () => + { + var results = new List(); + await foreach (var item in collection) + { + results.Add(item); + await Task.Delay(5); + } + return results; + }); + + var task2 = Task.Run(async () => + { + var results = new List(); + 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(1, 2, 3); + var collection = new LazyAsyncReadOnlyCollection(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(); + 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 CreateDelayedAsyncEnumerable( + IEnumerable items, + TimeSpan delay + ) + { + foreach (var item in items) + { + await Task.Delay(delay); + yield return item; + } + } +} diff --git a/tests/SharpCompress.Test/LazyReadOnlyCollectionTests.cs b/tests/SharpCompress.Test/LazyReadOnlyCollectionTests.cs new file mode 100644 index 00000000..e71dc3eb --- /dev/null +++ b/tests/SharpCompress.Test/LazyReadOnlyCollectionTests.cs @@ -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 : IEnumerable + { + private readonly List _items; + public int EnumerationCount { get; private set; } + public int ItemsRequestedCount { get; private set; } + + public TrackingEnumerable(params T[] items) + { + _items = new List(items); + } + + public IEnumerator GetEnumerator() + { + EnumerationCount++; + return new TrackingEnumerator(this); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => + GetEnumerator(); + + private class TrackingEnumerator : IEnumerator + { + private readonly TrackingEnumerable _parent; + private int _index = -1; + + public TrackingEnumerator(TrackingEnumerable 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(1, 2, 3, 4, 5); + var collection = new LazyReadOnlyCollection(source); + + // Act + var results = new List(); + 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("a", "b", "c"); + var collection = new LazyReadOnlyCollection(source); + + // Act - First enumeration + var firstResults = new List(); + foreach (var item in collection) + { + firstResults.Add(item); + } + + var itemsRequestedAfterFirst = source.ItemsRequestedCount; + + // Act - Second enumeration + var secondResults = new List(); + 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(10, 20, 30, 40); + var collection = new LazyReadOnlyCollection(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(1, 2, 3, 4, 5); + var collection = new LazyReadOnlyCollection(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(1, 2, 3, 4, 5); + var collection = new LazyReadOnlyCollection(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("apple", "banana", "cherry"); + var collection = new LazyReadOnlyCollection(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(10, 20, 30); + var collection = new LazyReadOnlyCollection(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(); + var collection = new LazyReadOnlyCollection(source); + + // Act + var results = new List(); + 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("only"); + var collection = new LazyReadOnlyCollection(source); + + // Act + var results = new List(); + 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(10, 20, 30, 40, 50); + var collection = new LazyReadOnlyCollection(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(1, 2, 3); + var collection = new LazyReadOnlyCollection(source); + + // Act & Assert + using var enumerator = collection.GetEnumerator(); + enumerator.MoveNext(); + Assert.Throws(() => enumerator.Reset()); + } + + [Fact] + public void Dispose_OnEnumerator_CompletesSuccessfully() + { + // Arrange + var source = new TrackingEnumerable(1, 2, 3); + var collection = new LazyReadOnlyCollection(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(); + 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(1, 2, 3); + var collection = new LazyReadOnlyCollection(source); + + // Act & Assert + Assert.True(collection.IsReadOnly); + } + + [Fact] + public void Add_ThrowsNotSupportedException() + { + // Arrange + var source = new TrackingEnumerable(1, 2, 3); + var collection = new LazyReadOnlyCollection(source); + + // Act & Assert + Assert.Throws(() => collection.Add(4)); + } + + [Fact] + public void Clear_ThrowsNotSupportedException() + { + // Arrange + var source = new TrackingEnumerable(1, 2, 3); + var collection = new LazyReadOnlyCollection(source); + + // Act & Assert + Assert.Throws(() => collection.Clear()); + } + + [Fact] + public void Remove_ThrowsNotSupportedException() + { + // Arrange + var source = new TrackingEnumerable(1, 2, 3); + var collection = new LazyReadOnlyCollection(source); + + // Act & Assert + Assert.Throws(() => collection.Remove(1)); + } + + [Fact] + public void NonGenericEnumerator_WorksCorrectly() + { + // Arrange + var source = new TrackingEnumerable(1, 2, 3); + var collection = new LazyReadOnlyCollection(source); + + // Act - Use non-generic IEnumerator + var results = new List(); + var enumerable = (System.Collections.IEnumerable)collection; + foreach (var item in enumerable) + { + results.Add((int)item); + } + + // Assert + Assert.Equal(new[] { 1, 2, 3 }, results); + } +}