File: ReadOnlyListExtensionsTests.cs
Web Access
Project: src\src\Razor\src\Shared\Microsoft.AspNetCore.Razor.Utilities.Shared.UnitTests\Microsoft.AspNetCore.Razor.Utilities.Shared.UnitTests.csproj (Microsoft.AspNetCore.Razor.Utilities.Shared.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using Xunit;
using SR = Microsoft.AspNetCore.Razor.Utilities.Shared.Resources.SR;
 
namespace Microsoft.AspNetCore.Razor.Utilities.Shared.Test;
 
public class ReadOnlyListExtensionsTest
{
    private static Func<int, bool> IsEven => x => x % 2 == 0;
    private static Func<int, bool> IsOdd => x => x % 2 != 0;
 
    [Fact]
    public void Any()
    {
        var list = new List<int>();
        var readOnlyList = (IReadOnlyList<int>)list;
 
        Assert.False(readOnlyList.Any());
 
        list.Add(19);
 
        Assert.True(readOnlyList.Any());
 
        list.Add(23);
 
        Assert.True(readOnlyList.Any(IsOdd));
 
        // ... but no even numbers
        Assert.False(readOnlyList.Any(IsEven));
    }
 
    [Fact]
    public void All()
    {
        var list = new List<int>();
        var readOnlyList = (IReadOnlyList<int>)list;
 
        Assert.True(readOnlyList.All(IsEven));
 
        list.Add(19);
 
        Assert.False(readOnlyList.All(IsEven));
 
        list.Add(23);
 
        Assert.True(readOnlyList.All(IsOdd));
 
        list.Add(42);
 
        Assert.False(readOnlyList.All(IsOdd));
    }
 
    [Fact]
    public void FirstAndLast()
    {
        var list = new List<int>();
        var readOnlyList = (IReadOnlyList<int>)list;
 
        var exception1 = Assert.Throws<InvalidOperationException>(() => readOnlyList.First());
        Assert.Equal(SR.Contains_no_elements, exception1.Message);
 
        Assert.Equal(default, readOnlyList.FirstOrDefault());
 
        var exception2 = Assert.Throws<InvalidOperationException>(() => readOnlyList.Last());
        Assert.Equal(SR.Contains_no_elements, exception1.Message);
 
        Assert.Equal(default, readOnlyList.LastOrDefault());
 
        list.Add(19);
 
        Assert.Equal(19, readOnlyList.First());
        Assert.Equal(19, readOnlyList.FirstOrDefault());
        Assert.Equal(19, readOnlyList.Last());
        Assert.Equal(19, readOnlyList.LastOrDefault());
 
        list.Add(23);
 
        Assert.Equal(19, readOnlyList.First());
        Assert.Equal(19, readOnlyList.FirstOrDefault());
        Assert.Equal(23, readOnlyList.Last());
        Assert.Equal(23, readOnlyList.LastOrDefault());
    }
 
    [Fact]
    public void FirstAndLastWithPredicate()
    {
        var list = new List<int>();
        var readOnlyList = (IReadOnlyList<int>)list;
 
        var exception1 = Assert.Throws<InvalidOperationException>(() => readOnlyList.First(IsOdd));
        Assert.Equal(SR.Contains_no_matching_elements, exception1.Message);
 
        Assert.Equal(default, readOnlyList.FirstOrDefault(IsOdd));
 
        var exception2 = Assert.Throws<InvalidOperationException>(() => readOnlyList.Last(IsOdd));
        Assert.Equal(SR.Contains_no_matching_elements, exception2.Message);
 
        Assert.Equal(default, readOnlyList.LastOrDefault(IsOdd));
 
        list.Add(19);
 
        Assert.Equal(19, readOnlyList.First(IsOdd));
        Assert.Equal(19, readOnlyList.FirstOrDefault(IsOdd));
        Assert.Equal(19, readOnlyList.Last(IsOdd));
        Assert.Equal(19, readOnlyList.LastOrDefault(IsOdd));
 
        list.Add(23);
 
        Assert.Equal(19, readOnlyList.First(IsOdd));
        Assert.Equal(19, readOnlyList.FirstOrDefault(IsOdd));
        Assert.Equal(23, readOnlyList.Last(IsOdd));
        Assert.Equal(23, readOnlyList.LastOrDefault(IsOdd));
 
        var exception3 = Assert.Throws<InvalidOperationException>(() => readOnlyList.First(IsEven));
        Assert.Equal(SR.Contains_no_matching_elements, exception3.Message);
 
        Assert.Equal(default, readOnlyList.FirstOrDefault(IsEven));
 
        var exception4 = Assert.Throws<InvalidOperationException>(() => readOnlyList.Last(IsEven));
        Assert.Equal(SR.Contains_no_matching_elements, exception4.Message);
 
        Assert.Equal(default, readOnlyList.LastOrDefault(IsEven));
 
        list.Add(42);
 
        Assert.Equal(42, readOnlyList.First(IsEven));
        Assert.Equal(42, readOnlyList.FirstOrDefault(IsEven));
        Assert.Equal(42, readOnlyList.Last(IsEven));
        Assert.Equal(42, readOnlyList.LastOrDefault(IsEven));
    }
 
    [Fact]
    public void Single()
    {
        var list = new List<int>();
        var readOnlyList = (IReadOnlyList<int>)list;
 
        var exception1 = Assert.Throws<InvalidOperationException>(() => readOnlyList.Single());
        Assert.Equal(SR.Contains_no_elements, exception1.Message);
        Assert.Equal(default, readOnlyList.SingleOrDefault());
 
        list.Add(19);
 
        Assert.Equal(19, readOnlyList.Single());
        Assert.Equal(19, readOnlyList.SingleOrDefault());
 
        list.Add(23);
 
        var exception2 = Assert.Throws<InvalidOperationException>(() => readOnlyList.Single());
        Assert.Equal(SR.Contains_more_than_one_element, exception2.Message);
        var exception3 = Assert.Throws<InvalidOperationException>(() => readOnlyList.SingleOrDefault());
        Assert.Equal(SR.Contains_more_than_one_element, exception2.Message);
    }
 
    [Fact]
    public void SingleWithPredicate()
    {
        var list = new List<int>();
        var readOnlyList = (IReadOnlyList<int>)list;
 
        var exception1 = Assert.Throws<InvalidOperationException>(() => readOnlyList.Single(IsOdd));
        Assert.Equal(SR.Contains_no_matching_elements, exception1.Message);
        Assert.Equal(default, readOnlyList.SingleOrDefault(IsOdd));
 
        list.Add(19);
 
        Assert.Equal(19, readOnlyList.Single(IsOdd));
        Assert.Equal(19, readOnlyList.SingleOrDefault(IsOdd));
 
        list.Add(23);
 
        var exception2 = Assert.Throws<InvalidOperationException>(() => readOnlyList.Single(IsOdd));
        Assert.Equal(SR.Contains_more_than_one_matching_element, exception2.Message);
        var exception3 = Assert.Throws<InvalidOperationException>(() => readOnlyList.SingleOrDefault(IsOdd));
        Assert.Equal(SR.Contains_more_than_one_matching_element, exception2.Message);
 
        list.Add(42);
 
        Assert.Equal(42, readOnlyList.Single(IsEven));
        Assert.Equal(42, readOnlyList.SingleOrDefault(IsEven));
    }
 
    [Fact]
    public void CopyTo_ImmutableArray()
    {
        Span<int> source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        var immutableArray = ImmutableArray.Create(source);
 
        AssertCopyToCore(immutableArray);
    }
 
    [Fact]
    public void CopyTo_ImmutableArrayBuilder()
    {
        Span<int> source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        var builder = ImmutableArray.CreateBuilder<int>();
        builder.AddRange(source);
 
        AssertCopyToCore(builder);
    }
 
    [Fact]
    public void CopyTo_List()
    {
        IEnumerable<int> source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        var list = new List<int>(source);
 
        AssertCopyToCore(list);
    }
 
    [Fact]
    public void CopyTo_Array()
    {
        int[] array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
 
        AssertCopyToCore(array);
    }
 
    [Fact]
    public void CopyTo_CustomReadOnlyList()
    {
        CustomReadOnlyList custom = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
 
        AssertCopyToCore(custom);
    }
 
    private static void AssertCopyToCore(IReadOnlyList<int> list)
    {
        var destination1 = new int[list.Count - 1];
        var exception = Assert.Throws<ArgumentException>(() => list.CopyTo(destination1.AsSpan()));
        Assert.StartsWith(SR.Destination_is_too_short, exception.Message);
 
        Span<int> destination2 = stackalloc int[list.Count];
        list.CopyTo(destination2);
        AssertElementsEqual(list, destination2);
 
        Span<int> destination3 = stackalloc int[list.Count + 1];
        list.CopyTo(destination3);
        AssertElementsEqual(list, destination3);
 
        static void AssertElementsEqual<T>(IReadOnlyList<T> list, ReadOnlySpan<T> span)
        {
            var count = list.Count;
            for (var i = 0; i < count; i++)
            {
                Assert.Equal(list[i], span[i]);
            }
        }
    }
 
    [CollectionBuilder(typeof(CustomReadOnlyList), "Create")]
    private sealed class CustomReadOnlyList(params ReadOnlySpan<int> values) : IReadOnlyList<int>
    {
        private readonly int[] _values = values.ToArray();
 
        public int this[int index] => _values[index];
        public int Count => _values.Length;
 
        public IEnumerator<int> GetEnumerator()
        {
            foreach (var value in _values)
            {
                yield return value;
            }
        }
 
        IEnumerator IEnumerable.GetEnumerator()
            => GetEnumerator();
 
        public static CustomReadOnlyList Create(ReadOnlySpan<int> span)
            => new(span);
    }
 
    [Fact]
    public void SelectAsArray()
    {
        IReadOnlyList<int> data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        ImmutableArray<int> expected = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20];
 
        var actual = data.SelectAsArray(static x => x * 2);
        Assert.Equal<int>(expected, actual);
    }
 
    [Fact]
    public void SelectAsArray_Enumerable()
    {
        IReadOnlyList<int> data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        ImmutableArray<int> expected = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20];
 
        var enumerable = (IEnumerable<int>)data;
 
        var actual = enumerable.SelectAsArray(static x => x * 2);
        Assert.Equal<int>(expected, actual);
    }
 
    [Fact]
    public void SelectAsArray_Index()
    {
        IReadOnlyList<int> data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        ImmutableArray<int> expected = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
 
        var actual = data.SelectAsArray(static (x, index) => x + index);
        Assert.Equal<int>(expected, actual);
    }
 
    [Fact]
    public void SelectAsArray_Index_Enumerable()
    {
        IReadOnlyList<int> data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        ImmutableArray<int> expected = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
 
        var enumerable = (IEnumerable<int>)data;
 
        var actual = enumerable.SelectAsArray(static (x, index) => x + index);
        Assert.Equal<int>(expected, actual);
    }
 
    [Fact]
    public void AsEnumerable_Basic()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable());
 
        Assert.Equal([1, 2, 3, 4, 5], result);
    }
 
    [Fact]
    public void AsEnumerable_Basic_EmptyList()
    {
        IReadOnlyList<int> list = [];
 
        var result = Enumerate(list.AsEnumerable());
 
        Assert.Empty(result);
    }
 
    [Fact]
    public void AsEnumerable_WithStart()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(start: 2));
 
        Assert.Equal([3, 4, 5], result);
    }
 
    [Fact]
    public void AsEnumerable_WithStart_StartAtEnd()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(start: 5));
 
        Assert.Empty(result);
    }
 
    [Fact]
    public void AsEnumerable_WithStart_StartAtZero()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(start: 0));
 
        Assert.Equal([1, 2, 3, 4, 5], result);
    }
 
    [Fact]
    public void AsEnumerable_WithStartIndex_FromStart()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(^3)); // Last 3 elements
 
        Assert.Equal([3, 4, 5], result);
    }
 
    [Fact]
    public void AsEnumerable_WithStartIndex_FromEnd()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(^1)); // Last element
 
        Assert.Equal([5], result);
    }
 
    [Fact]
    public void AsEnumerable_WithRange()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(1..4)); // Elements at indices 1, 2, 3
 
        Assert.Equal([2, 3, 4], result);
    }
 
    [Fact]
    public void AsEnumerable_WithRange_FromEnd()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(^3..^1)); // Last 3 elements excluding the very last
 
        Assert.Equal([3, 4], result);
    }
 
    [Fact]
    public void AsEnumerable_WithRange_EntireRange()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(..)); // Entire range
 
        Assert.Equal([1, 2, 3, 4, 5], result);
    }
 
    [Fact]
    public void AsEnumerable_WithStartAndCount()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(start: 1, count: 3));
 
        Assert.Equal([2, 3, 4], result);
    }
 
    [Fact]
    public void AsEnumerable_WithStartAndCount_ZeroCount()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(start: 2, count: 0));
 
        Assert.Empty(result);
    }
 
    [Fact]
    public void AsEnumerable_WithStartAndCount_SingleElement()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(start: 3, count: 1));
 
        Assert.Equal([4], result);
    }
 
    [Fact]
    public void AsEnumerable_Reversed()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable().Reversed());
 
        Assert.Equal([5, 4, 3, 2, 1], result);
    }
 
    [Fact]
    public void AsEnumerable_Reversed_WithRange()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        var result = Enumerate(list.AsEnumerable(start: 1, count: 3).Reversed());
 
        Assert.Equal([4, 3, 2], result);
    }
 
    [Fact]
    public void AsEnumerable_Reversed_EmptyList()
    {
        IReadOnlyList<int> list = [];
 
        var result = Enumerate(list.AsEnumerable().Reversed());
 
        Assert.Empty(result);
    }
 
    [Fact]
    public void AsEnumerable_EnumeratorReset()
    {
        IReadOnlyList<int> list = [1, 2, 3];
        var enumerable = list.AsEnumerable();
        var enumerator = enumerable.GetEnumerator();
 
        // First enumeration
        var firstPass = new List<int>();
        while (enumerator.MoveNext())
        {
            firstPass.Add(enumerator.Current);
        }
 
        Assert.Equal([1, 2, 3], firstPass);
 
        // Reset and enumerate again
        enumerator.Reset();
 
        var secondPass = new List<int>();
        while (enumerator.MoveNext())
        {
            secondPass.Add(enumerator.Current);
        }
 
        Assert.Equal([1, 2, 3], secondPass);
    }
 
    [Fact]
    public void AsEnumerable_ReverseEnumeratorReset()
    {
        IReadOnlyList<int> list = [1, 2, 3];
        var enumerable = list.AsEnumerable().Reversed();
        var enumerator = enumerable.GetEnumerator();
 
        // First enumeration
        var firstPass = new List<int>();
        while (enumerator.MoveNext())
        {
            firstPass.Add(enumerator.Current);
        }
 
        Assert.Equal([3, 2, 1], firstPass);
 
        // Reset and enumerate again
        enumerator.Reset();
 
        var secondPass = new List<int>();
        while (enumerator.MoveNext())
        {
            secondPass.Add(enumerator.Current);
        }
 
        Assert.Equal([3, 2, 1], secondPass);
    }
 
    [Fact]
    public void AsEnumerable_ImmutableArray()
    {
        ImmutableArray<int> array = [1, 2, 3, 4, 5];
        IReadOnlyList<int> list = array;
 
        var result = Enumerate(list.AsEnumerable(start: 1, count: 3));
 
        Assert.Equal([2, 3, 4], result);
    }
 
    [Fact]
    public void AsEnumerable_CustomReadOnlyList()
    {
        CustomReadOnlyList custom = [1, 2, 3, 4, 5];
 
        var result = Enumerate(custom.AsEnumerable(start: 1, count: 3));
 
        Assert.Equal([2, 3, 4], result);
    }
 
    [Fact]
    public void AsEnumerable_NullList_ThrowsArgumentNullException()
    {
        IReadOnlyList<int> list = null!;
 
        Assert.Throws<ArgumentNullException>(() => list.AsEnumerable());
        Assert.Throws<ArgumentNullException>(() => list.AsEnumerable(start: 0));
        Assert.Throws<ArgumentNullException>(() => list.AsEnumerable(^1));
        Assert.Throws<ArgumentNullException>(() => list.AsEnumerable(0..3));
        Assert.Throws<ArgumentNullException>(() => list.AsEnumerable(start: 0, count: 1));
    }
 
    [Fact]
    public void AsEnumerable_NegativeStart_ThrowsArgumentOutOfRangeException()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: -1));
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: -5, count: 2));
    }
 
    [Fact]
    public void AsEnumerable_NegativeCount_ThrowsArgumentOutOfRangeException()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: 0, count: -1));
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: 2, count: -3));
    }
 
    [Fact]
    public void AsEnumerable_StartOutOfRange_ThrowsArgumentOutOfRangeException()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: 6));
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: 10, count: 1));
    }
 
    [Fact]
    public void AsEnumerable_StartPlusCountExceedsLength_ThrowsArgumentOutOfRangeException()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: 3, count: 3)); // 3 + 3 = 6 > 5
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: 0, count: 6)); // 0 + 6 = 6 > 5
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: 5, count: 1)); // 5 + 1 = 6 > 5
    }
 
    [Fact]
    public void AsEnumerable_ValidRangeAtBoundaries_Succeeds()
    {
        IReadOnlyList<int> list = [1, 2, 3, 4, 5];
 
        // Valid boundary cases that should not throw
        var result1 = list.AsEnumerable(start: 0, count: 0); // Empty range at start
        Assert.Empty(Enumerate(result1));
 
        var result2 = list.AsEnumerable(start: 5, count: 0); // Empty range at end
        Assert.Empty(Enumerate(result2));
 
        var result3 = list.AsEnumerable(start: 0, count: 5); // Full range
        Assert.Equal([1, 2, 3, 4, 5], Enumerate(result3));
 
        var result4 = list.AsEnumerable(start: 4, count: 1); // Last element
        Assert.Equal([5], Enumerate(result4));
    }
 
    [Fact]
    public void AsEnumerable_EmptyList_ValidArguments_Succeeds()
    {
        IReadOnlyList<int> list = [];
 
        // These should not throw on empty list
        var result1 = list.AsEnumerable();
        Assert.Empty(Enumerate(result1));
 
        var result2 = list.AsEnumerable(start: 0);
        Assert.Empty(Enumerate(result2));
 
        var result3 = list.AsEnumerable(start: 0, count: 0);
        Assert.Empty(Enumerate(result3));
 
        var result4 = list.AsEnumerable(..);
        Assert.Empty(Enumerate(result4));
    }
 
    [Fact]
    public void AsEnumerable_EmptyList_InvalidArguments_ThrowsArgumentOutOfRangeException()
    {
        IReadOnlyList<int> list = [];
 
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: 1));
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: 0, count: 1));
        Assert.Throws<ArgumentOutOfRangeException>(() => list.AsEnumerable(start: 1, count: 0));
    }
 
    private static T[] Enumerate<T>(ReadOnlyListExtensions.Enumerable<T> enumerable)
    {
        var result = new List<T>();
 
        foreach (var item in enumerable)
        {
            result.Add(item);
        }
 
        return [.. result];
    }
 
    private static T[] Enumerate<T>(ReadOnlyListExtensions.Enumerable<T>.ReversedEnumerable enumerable)
    {
        var result = new List<T>();
 
        foreach (var item in enumerable)
        {
            result.Add(item);
        }
 
        return [.. result];
    }
}