diff --git a/src/Common/tests/System/Collections/DelegateEqualityComparer.cs b/src/Common/tests/System/Collections/DelegateEqualityComparer.cs new file mode 100644 index 000000000000..c22ed45883be --- /dev/null +++ b/src/Common/tests/System/Collections/DelegateEqualityComparer.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace System.Collections.Tests +{ + internal sealed class DelegateEqualityComparer : IEqualityComparer, IEqualityComparer + { + private readonly Func _equals; + private readonly Func _getHashCode; + private readonly Func _objectEquals; + private readonly Func _objectGetHashCode; + + public DelegateEqualityComparer( + Func equals = null, + Func getHashCode = null, + Func objectEquals = null, + Func objectGetHashCode = null) + { + _equals = equals ?? ((x, y) => { throw new NotImplementedException(); }); + _getHashCode = getHashCode ?? (obj => { throw new NotImplementedException(); }); + _objectEquals = objectEquals ?? ((x, y) => { throw new NotImplementedException(); }); + _objectGetHashCode = objectGetHashCode ?? (obj => { throw new NotImplementedException(); }); + } + + public bool Equals(T x, T y) => _equals(x, y); + + public int GetHashCode(T obj) => _getHashCode(obj); + + bool IEqualityComparer.Equals(object x, object y) => _objectEquals(x, y); + + int IEqualityComparer.GetHashCode(object obj) => _objectGetHashCode(obj); + } +} diff --git a/src/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableArray_1.cs b/src/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableArray_1.cs index f377e9a3efa2..6e8ab3208862 100644 --- a/src/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableArray_1.cs +++ b/src/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableArray_1.cs @@ -170,7 +170,7 @@ public bool IsEmpty } /// - /// Gets the number of array in the collection. + /// Gets the number of elements in the array. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] public int Length diff --git a/src/System.Collections.Immutable/tests/ImmutableArrayTest.cs b/src/System.Collections.Immutable/tests/ImmutableArrayTest.cs index bdea30f01c3c..4895474e34ae 100644 --- a/src/System.Collections.Immutable/tests/ImmutableArrayTest.cs +++ b/src/System.Collections.Immutable/tests/ImmutableArrayTest.cs @@ -3,8 +3,11 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Collections.Tests; using System.Diagnostics; using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Xunit; @@ -20,239 +23,325 @@ public class ImmutableArrayTest : SimpleElementImmutablesTestBase private static readonly ImmutableArray s_oneElementRefType = ImmutableArray.Create(new GenericParameterHelper(1)); private static readonly ImmutableArray s_twoElementRefTypeWithNull = ImmutableArray.Create("1", null); - [Fact] - public void Clear() + public static IEnumerable Int32EnumerableData() { - Assert.Equal(ImmutableArray.Empty, ImmutableArray.Create().Clear()); - Assert.Equal(ImmutableArray.Empty, ImmutableArray.Create(1).Clear()); - Assert.Equal(ImmutableArray.Empty, ImmutableArray.Create(1, 2, 3).Clear()); + yield return new object[] { new int[0] }; + yield return new object[] { new[] { 1 } }; + yield return new object[] { Enumerable.Range(1, 3) }; + yield return new object[] { Enumerable.Range(4, 4) }; + yield return new object[] { new[] { 2, 3, 5 } }; } - [Fact] - public void CreateEmpty() + public static IEnumerable StrongBoxedInt32EnumerableData() { - Assert.Equal(ImmutableArray.Empty, ImmutableArray.Create()); - Assert.Equal(ImmutableArray.Empty, ImmutableArray.Create(new int[0])); + // Once https://github.com/xunit/assert.xunit/pull/5 comes into corefx, all the StrongBox stuff can be removed. + + return Int32EnumerableData() + .Select(array => array[0]) + .Cast>() + .Select(enumerable => new object[] + { + new StrongBox>(enumerable) + }); } - [Fact] - public void CreateFromEnumerable() + public static IEnumerable StrongBoxedSpecialInt32ImmutableArrayData() { - Assert.Throws("items", () => ImmutableArray.CreateRange((IEnumerable)null)); + // Once https://github.com/xunit/assert.xunit/pull/5 comes into corefx, all the StrongBox stuff can be removed. - IEnumerable source = new[] { 1, 2, 3 }; - var array = ImmutableArray.CreateRange(source); - Assert.Equal(3, array.Length); + yield return new object[] { new StrongBox>(s_emptyDefault) }; + yield return new object[] { new StrongBox>(s_empty) }; } - [Fact] - public void CreateFromEmptyEnumerableReturnsSingleton() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void Clear(IEnumerable source) { - IEnumerable emptySource1 = new int[0]; - var immutable = ImmutableArray.CreateRange(emptySource1); - - // This equality check returns true if the underlying arrays are the same instance. - Assert.Equal(s_empty, immutable); + Assert.True(s_empty == source.ToImmutableArray().Clear()); } [Fact] - public void CreateRangeFromImmutableArrayWithSelector() + public void CreateEnumerableElementType() { - var array = ImmutableArray.Create(4, 5, 6, 7); - - var copy1 = ImmutableArray.CreateRange(array, i => i + 0.5); - Assert.Equal(new[] { 4.5, 5.5, 6.5, 7.5 }, copy1); + // Create should not have the same semantics as CreateRange, except for arrays. + // If you pass in an IEnumerable to Create, you should get an + // ImmutableArray>. However, if you pass a T[] in, you should get + // a ImmutableArray. - var copy2 = ImmutableArray.CreateRange(array, i => i + 1); - Assert.Equal(new[] { 5, 6, 7, 8 }, copy2); + var array = new int[0]; + Assert.IsType>(ImmutableArray.Create(array)); - Assert.Equal(new int[] { }, ImmutableArray.CreateRange(s_empty, i => i)); + var immutable = ImmutableArray.Empty; + Assert.IsType>>(ImmutableArray.Create(immutable)); - Assert.Throws("selector", () => ImmutableArray.CreateRange(array, (Func)null)); + var enumerable = Enumerable.Empty(); + Assert.IsType>>(ImmutableArray.Create(enumerable)); } [Fact] - public void CreateRangeFromImmutableArrayWithSelectorAndArgument() + public void CreateEmpty() { - var array = ImmutableArray.Create(4, 5, 6, 7); - - var copy1 = ImmutableArray.CreateRange(array, (i, j) => i + j, 0.5); - Assert.Equal(new[] { 4.5, 5.5, 6.5, 7.5 }, copy1); - - var copy2 = ImmutableArray.CreateRange(array, (i, j) => i + j, 1); - Assert.Equal(new[] { 5, 6, 7, 8 }, copy2); - - var copy3 = ImmutableArray.CreateRange(array, (int i, object j) => i, null); - Assert.Equal(new[] { 4, 5, 6, 7 }, copy3); + Assert.True(s_empty == ImmutableArray.Create()); + Assert.True(s_empty == ImmutableArray.Create(new int[0])); + } - Assert.Equal(new int[] { }, ImmutableArray.CreateRange(s_empty, (i, j) => i + j, 0)); + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateRange(IEnumerable source) + { + Assert.Equal(source, ImmutableArray.CreateRange(source)); + } - Assert.Throws("selector", () => ImmutableArray.CreateRange(array, (Func)null, 0)); + [Fact] + public void CreateRangeInvalid() + { + Assert.Throws("items", () => ImmutableArray.CreateRange((IEnumerable)null)); } [Fact] - public void CreateRangeSliceFromImmutableArrayWithSelector() + public void CreateRangeEmptyReturnsSingleton() { - var array = ImmutableArray.Create(4, 5, 6, 7); + var empty = ImmutableArray.CreateRange(new int[0]); + // This equality check returns true if the underlying arrays are the same instance. + Assert.True(s_empty == empty); + } - var copy1 = ImmutableArray.CreateRange(array, 0, 0, i => i + 0.5); - Assert.Equal(new double[] { }, copy1); + [Theory] + [MemberData(nameof(CreateRangeWithSelectorData))] + public void CreateRangeWithSelector(IEnumerable source, Func selector, TResult dummy) + { + // Remove the dummy parameters once https://github.com/xunit/xunit/pull/965 makes it into corefx. - var copy2 = ImmutableArray.CreateRange(array, 0, 0, i => i); - Assert.Equal(new int[] { }, copy2); + Assert.Equal(source.Select(selector), ImmutableArray.CreateRange(source.ToImmutableArray(), selector)); + } - var copy3 = ImmutableArray.CreateRange(array, 0, 1, i => i * 2); - Assert.Equal(new int[] { 8 }, copy3); + public static IEnumerable CreateRangeWithSelectorData() + { + yield return new object[] { new int[] { }, new Func(i => i), 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, new Func(i => i + 0.5f), 0f }; + yield return new object[] { new[] { 4, 5, 6, 7 }, new Func(i => i + 1), 0 }; + } - var copy4 = ImmutableArray.CreateRange(array, 0, 2, i => i + 1); - Assert.Equal(new int[] { 5, 6 }, copy4); + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateRangeWithSelectorInvalid(IEnumerable source) + { + Assert.Throws("selector", () => ImmutableArray.CreateRange(source.ToImmutableArray(), (Func)null)); + // If both parameters are invalid, the selector should be validated first. + Assert.Throws("selector", () => ImmutableArray.CreateRange(s_emptyDefault, (Func)null)); + Assert.Throws(() => ImmutableArray.CreateRange(s_emptyDefault, i => i)); + } - var copy5 = ImmutableArray.CreateRange(array, 0, 4, i => i); - Assert.Equal(new int[] { 4, 5, 6, 7 }, copy5); + [Theory] + [MemberData(nameof(CreateRangeWithSelectorAndArgumentData))] + public void CreateRangeWithSelectorAndArgument(IEnumerable source, Func selector, TArg arg, TArg dummy1, TResult dummy2) + { + // Remove the dummy parameters once https://github.com/xunit/xunit/pull/965 makes it into corefx. - var copy6 = ImmutableArray.CreateRange(array, 3, 1, i => i); - Assert.Equal(new int[] { 7 }, copy6); + var expected = source.Zip(Enumerable.Repeat(arg, source.Count()), selector); + Assert.Equal(expected, ImmutableArray.CreateRange(source.ToImmutableArray(), selector, arg)); + } - var copy7 = ImmutableArray.CreateRange(array, 3, 0, i => i); - Assert.Equal(new int[] { }, copy7); + public static IEnumerable CreateRangeWithSelectorAndArgumentData() + { + yield return new object[] { new int[] { }, new Func((x, y) => x + y), 0, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, new Func((x, y) => x + y), 0.5f, 0f, 0f }; + yield return new object[] { new[] { 4, 5, 6, 7 }, new Func((x, y) => x + y), 1, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, new Func((x, y) => x), null, new object(), 0 }; + } - var copy8 = ImmutableArray.CreateRange(array, 4, 0, i => i); - Assert.Equal(new int[] { }, copy8); + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateRangeWithSelectorAndArgumentInvalid(IEnumerable source) + { + Assert.Throws("selector", () => ImmutableArray.CreateRange(source.ToImmutableArray(), (Func)null, 0)); + // If both parameters are invalid, the selector should be validated first. + Assert.Throws("selector", () => ImmutableArray.CreateRange(s_emptyDefault, (Func)null, 0)); + Assert.Throws(() => ImmutableArray.CreateRange(s_emptyDefault, (x, y) => 0, 0)); + } - Assert.Throws("selector", () => ImmutableArray.CreateRange(array, 0, 0, (Func)null)); - Assert.Throws("selector", () => ImmutableArray.CreateRange(s_empty, 0, 0, (Func)null)); + [Theory] + [MemberData(nameof(CreateRangeSliceWithSelectorData))] + public void CreateRangeSliceWithSelector(IEnumerable source, int start, int length, Func selector, TResult dummy) + { + // Remove the dummy parameters once https://github.com/xunit/xunit/pull/965 makes it into corefx. - Assert.Throws("start", () => ImmutableArray.CreateRange(array, -1, 1, (Func)null)); - Assert.Throws("start", () => ImmutableArray.CreateRange(array, -1, 1, i => i)); - Assert.Throws("length", () => ImmutableArray.CreateRange(array, 0, 5, i => i)); - Assert.Throws("length", () => ImmutableArray.CreateRange(array, 4, 1, i => i)); - Assert.Throws("length", () => ImmutableArray.CreateRange(array, 3, 2, i => i)); - Assert.Throws("length", () => ImmutableArray.CreateRange(array, 1, -1, i => i)); + var expected = source.Skip(start).Take(length).Select(selector); + Assert.Equal(expected, ImmutableArray.CreateRange(source.ToImmutableArray(), start, length, selector)); } - [Fact] - public void CreateRangeSliceFromImmutableArrayWithSelectorAndArgument() + public static IEnumerable CreateRangeSliceWithSelectorData() { - var array = ImmutableArray.Create(4, 5, 6, 7); - - var copy1 = ImmutableArray.CreateRange(array, 0, 0, (i, j) => i + j, 0.5); - Assert.Equal(new double[] { }, copy1); + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 0, new Func(i => i + 0.5f), 0f }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 0, new Func(i => i + 0.5d), 0d }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 0, new Func(i => i), 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 1, new Func(i => i * 2), 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 2, new Func(i => i + 1), 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 4, new Func(i => i), 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 3, 1, new Func(i => i), 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 3, 0, new Func(i => i), 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 4, 0, new Func(i => i), 0 }; + } - var copy2 = ImmutableArray.CreateRange(array, 0, 0, (i, j) => i + j, 0); - Assert.Equal(new int[] { }, copy2); + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateRangeSliceWithSelectorInvalid(IEnumerable source) + { + var array = source.ToImmutableArray(); - var copy3 = ImmutableArray.CreateRange(array, 0, 1, (i, j) => i * j, 2); - Assert.Equal(new int[] { 8 }, copy3); + Assert.Throws("selector", () => ImmutableArray.CreateRange(array, 0, 0, (Func)null)); - var copy4 = ImmutableArray.CreateRange(array, 0, 2, (i, j) => i + j, 1); - Assert.Equal(new int[] { 5, 6 }, copy4); + Assert.Throws("start", () => ImmutableArray.CreateRange(array, -1, 1, (Func)null)); + Assert.Throws("start", () => ImmutableArray.CreateRange(array, -1, 1, i => i)); - var copy5 = ImmutableArray.CreateRange(array, 0, 4, (i, j) => i + j, 0); - Assert.Equal(new int[] { 4, 5, 6, 7 }, copy5); + Assert.Throws("length", () => ImmutableArray.CreateRange(array, 0, array.Length + 1, i => i)); + Assert.Throws("length", () => ImmutableArray.CreateRange(array, array.Length, 1, i => i)); + Assert.Throws("length", () => ImmutableArray.CreateRange(array, Math.Max(0, array.Length - 1), 2, i => i)); + Assert.Throws("length", () => ImmutableArray.CreateRange(array, 0, -1, i => i)); - var copy6 = ImmutableArray.CreateRange(array, 3, 1, (i, j) => i + j, 0); - Assert.Equal(new int[] { 7 }, copy6); + Assert.Throws(() => ImmutableArray.CreateRange(s_emptyDefault, 0, 0, i => i)); + } - var copy7 = ImmutableArray.CreateRange(array, 3, 0, (i, j) => i + j, 0); - Assert.Equal(new int[] { }, copy7); + [Theory] + [MemberData(nameof(CreateRangeSliceWithSelectorAndArgumentData))] + public void CreateRangeSliceWithSelectorAndArgument(IEnumerable source, int start, int length, Func selector, TArg arg, TArg dummy1, TResult dummy2) + { + // Remove the dummy parameters once https://github.com/xunit/xunit/pull/965 makes it into corefx. - var copy8 = ImmutableArray.CreateRange(array, 4, 0, (i, j) => i + j, 0); - Assert.Equal(new int[] { }, copy8); + var expected = source.Skip(start).Take(length).Zip(Enumerable.Repeat(arg, length), selector); + Assert.Equal(expected, ImmutableArray.CreateRange(source.ToImmutableArray(), start, length, selector, arg)); + } - var copy9 = ImmutableArray.CreateRange(array, 0, 1, (int i, object j) => i, null); - Assert.Equal(new int[] { 4 }, copy9); + public static IEnumerable CreateRangeSliceWithSelectorAndArgumentData() + { + yield return new object[] { new int[] { }, 0, 0, new Func((x, y) => x + y), 0, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 0, new Func((x, y) => x + y), 0.5f, 0f, 0f }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 0, new Func((x, y) => x + y), 0.5d, 0d, 0d }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 0, new Func((x, y) => x + y), 0, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 1, new Func((x, y) => x * y), 2, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 2, new Func((x, y) => x + y), 1, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 4, new Func((x, y) => x + y), 0, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 3, 1, new Func((x, y) => x + y), 0, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 3, 0, new Func((x, y) => x + y), 0, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 4, 0, new Func((x, y) => x + y), 0, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 1, new Func((x, y) => x), null, new object(), 0 }; + } - Assert.Equal(new int[] { }, ImmutableArray.CreateRange(s_empty, 0, 0, (i, j) => i + j, 0)); + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateRangeSliceWithSelectorAndArgumentInvalid(IEnumerable source) + { + var array = source.ToImmutableArray(); Assert.Throws("selector", () => ImmutableArray.CreateRange(array, 0, 0, (Func)null, 0)); - Assert.Throws("selector", () => ImmutableArray.CreateRange(s_empty, 0, 0, (Func)null, 0)); Assert.Throws("start", () => ImmutableArray.CreateRange(s_empty, -1, 1, (Func)null, 0)); Assert.Throws("start", () => ImmutableArray.CreateRange(array, -1, 1, (i, j) => i + j, 0)); - Assert.Throws("length", () => ImmutableArray.CreateRange(array, 0, 5, (i, j) => i + j, 0)); - Assert.Throws("length", () => ImmutableArray.CreateRange(array, 4, 1, (i, j) => i + j, 0)); - Assert.Throws("length", () => ImmutableArray.CreateRange(array, 3, 2, (i, j) => i + j, 0)); - Assert.Throws("length", () => ImmutableArray.CreateRange(array, 1, -1, (i, j) => i + j, 0)); + + Assert.Throws("length", () => ImmutableArray.CreateRange(array, 0, array.Length + 1, (i, j) => i + j, 0)); + Assert.Throws("length", () => ImmutableArray.CreateRange(array, array.Length, 1, (i, j) => i + j, 0)); + Assert.Throws("length", () => ImmutableArray.CreateRange(array, Math.Max(0, array.Length - 1), 2, (i, j) => i + j, 0)); + Assert.Throws("length", () => ImmutableArray.CreateRange(array, 0, -1, (i, j) => i + j, 0)); + + Assert.Throws(() => ImmutableArray.CreateRange(s_emptyDefault, 0, 0, (x, y) => 0, 0)); } - [Fact] - public void CreateFromSliceOfImmutableArray() + [Theory] + [MemberData(nameof(CreateFromSliceData))] + public void CreateFromSlice(IEnumerable source, int start, int length) + { + Assert.Equal(source.Skip(start).Take(length), ImmutableArray.Create(source.ToImmutableArray(), start, length)); + Assert.Equal(source.Skip(start).Take(length), ImmutableArray.Create(source.ToArray(), start, length)); + } + + public static IEnumerable CreateFromSliceData() { - var array = ImmutableArray.Create(4, 5, 6, 7); - Assert.Equal(new[] { 4, 5 }, ImmutableArray.Create(array, 0, 2)); - Assert.Equal(new[] { 5, 6 }, ImmutableArray.Create(array, 1, 2)); - Assert.Equal(new[] { 6, 7 }, ImmutableArray.Create(array, 2, 2)); - Assert.Equal(new[] { 7 }, ImmutableArray.Create(array, 3, 1)); - Assert.Equal(new int[0], ImmutableArray.Create(array, 4, 0)); + yield return new object[] { new int[] { }, 0, 0 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 0, 2 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 1, 2 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 2, 2 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 3, 1 }; + yield return new object[] { new[] { 4, 5, 6, 7 }, 4, 0 }; + } - Assert.Equal(new int[] { }, ImmutableArray.Create(s_empty, 0, 0)); + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateFromSliceOfImmutableArrayInvalid(IEnumerable source) + { + var array = source.ToImmutableArray(); - Assert.Throws("length", () => ImmutableArray.Create(s_empty, 0, 1)); Assert.Throws("start", () => ImmutableArray.Create(array, -1, 0)); + Assert.Throws("start", () => ImmutableArray.Create(array, array.Length + 1, 0)); + Assert.Throws("length", () => ImmutableArray.Create(array, 0, -1)); Assert.Throws("length", () => ImmutableArray.Create(array, 0, array.Length + 1)); - Assert.Throws("length", () => ImmutableArray.Create(array, 1, array.Length)); - Assert.Throws("start", () => ImmutableArray.Create(array, array.Length + 1, 0)); + Assert.Throws("length", () => ImmutableArray.Create(array, Math.Max(0, array.Length - 1), 2)); + + if (array.Length > 0) + { + Assert.Throws("length", () => ImmutableArray.Create(array, 1, array.Length)); + } } - [Fact] - public void CreateFromSliceOfImmutableArrayOptimizations() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateFromSliceOfImmutableArrayOptimizations(IEnumerable source) { - var array = ImmutableArray.Create(4, 5, 6, 7); + var array = source.ToImmutableArray(); var slice = ImmutableArray.Create(array, 0, array.Length); - Assert.Equal(array, slice); // array instance actually shared between the two + Assert.True(array == slice); // Verify that the underlying arrays are reference-equal. } - [Fact] - public void CreateFromSliceOfImmutableArrayEmptyReturnsSingleton() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateFromSliceOfImmutableArrayEmptyReturnsSingleton(IEnumerable source) { - var array = ImmutableArray.Create(4, 5, 6, 7); - var slice = ImmutableArray.Create(array, 1, 0); - Assert.Equal(s_empty, slice); + var array = source.ToImmutableArray(); + var slice = ImmutableArray.Create(array, Math.Min(1, array.Length), 0); + Assert.True(s_empty == slice); } - [Fact] - public void CreateFromSliceOfArray() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateFromSliceOfArrayInvalid(IEnumerable source) { - var array = new int[] { 4, 5, 6, 7 }; - Assert.Equal(new[] { 4, 5 }, ImmutableArray.Create(array, 0, 2)); - Assert.Equal(new[] { 5, 6 }, ImmutableArray.Create(array, 1, 2)); - Assert.Equal(new[] { 6, 7 }, ImmutableArray.Create(array, 2, 2)); - Assert.Equal(new[] { 7 }, ImmutableArray.Create(array, 3, 1)); - Assert.Equal(new int[0], ImmutableArray.Create(array, 4, 0)); + var array = source.ToArray(); Assert.Throws("start", () => ImmutableArray.Create(array, -1, 0)); + Assert.Throws("start", () => ImmutableArray.Create(array, array.Length + 1, 0)); + Assert.Throws("length", () => ImmutableArray.Create(array, 0, -1)); Assert.Throws("length", () => ImmutableArray.Create(array, 0, array.Length + 1)); - Assert.Throws("length", () => ImmutableArray.Create(array, 1, array.Length)); - Assert.Throws("start", () => ImmutableArray.Create(array, array.Length + 1, 0)); + Assert.Throws("length", () => ImmutableArray.Create(array, Math.Max(0, array.Length - 1), 2)); + + if (array.Length > 0) + { + Assert.Throws("length", () => ImmutableArray.Create(array, 1, array.Length)); + } } - [Fact] - public void CreateFromSliceOfArrayEmptyReturnsSingleton() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateFromSliceOfArrayEmptyReturnsSingleton(IEnumerable source) { - var array = new int[] { 4, 5, 6, 7 }; - var slice = ImmutableArray.Create(array, 1, 0); - Assert.Equal(s_empty, slice); - slice = ImmutableArray.Create(array, array.Length, 0); - Assert.Equal(s_empty, slice); + var array = source.ToArray(); + var slice = ImmutableArray.Create(array, Math.Min(1, array.Length), 0); + Assert.True(s_empty == slice); } - [Fact] - public void CreateFromArray() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CreateFromArray(IEnumerable source) { - var source = new[] { 1, 2, 3 }; - var immutable = ImmutableArray.Create(source); - Assert.Equal(source, immutable); + Assert.Equal(source, ImmutableArray.Create(source.ToArray())); } [Fact] - public void CreateFromNullArray() + public void CreateFromArrayNull() { - int[] nullArray = null; - ImmutableArray immutable = ImmutableArray.Create(nullArray); + var immutable = ImmutableArray.Create(default(int[])); Assert.False(immutable.IsDefault); - Assert.Equal(0, immutable.Length); + Assert.True(immutable.IsEmpty); } [Fact] @@ -261,13 +350,13 @@ public void Covariance() ImmutableArray derivedImmutable = ImmutableArray.Create("a", "b", "c"); ImmutableArray baseImmutable = derivedImmutable.As(); Assert.False(baseImmutable.IsDefault); - // Must cast to object or the IEnumerable overload of Equals would be used + // Must cast to object or the IEnumerable overload of Assert.Equal would be used Assert.Equal((object)derivedImmutable, baseImmutable, EqualityComparer.Default); // Make sure we can reverse that, as a means to verify the underlying array is the same instance. ImmutableArray derivedImmutable2 = baseImmutable.As(); Assert.False(derivedImmutable2.IsDefault); - Assert.Equal(derivedImmutable, derivedImmutable2); + Assert.True(derivedImmutable == derivedImmutable2); // Try a cast that would fail. Assert.True(baseImmutable.As().IsDefault); @@ -287,13 +376,11 @@ public void DowncastOfDefaultStructs() Assert.True(derivedImmutable == derivedImmutable2); } - /// - /// Verifies that using an ordinary Create factory method is smart enough to reuse - /// an underlying array when possible. - /// [Fact] public void CovarianceImplicit() { + // Verify that CreateRange is smart enough to reuse the underlying array when possible. + ImmutableArray derivedImmutable = ImmutableArray.Create("a", "b", "c"); ImmutableArray baseImmutable = ImmutableArray.CreateRange(derivedImmutable); // Must cast to object or the IEnumerable overload of Equals would be used @@ -301,7 +388,7 @@ public void CovarianceImplicit() // Make sure we can reverse that, as a means to verify the underlying array is the same instance. ImmutableArray derivedImmutable2 = baseImmutable.As(); - Assert.Equal(derivedImmutable, derivedImmutable2); + Assert.True(derivedImmutable == derivedImmutable2); } [Fact] @@ -313,8 +400,8 @@ public void CastUpReference() Assert.Equal((object)derivedImmutable, baseImmutable, EqualityComparer.Default); // Make sure we can reverse that, as a means to verify the underlying array is the same instance. - Assert.Equal(derivedImmutable, baseImmutable.As()); - Assert.Equal(derivedImmutable, baseImmutable.CastArray()); + Assert.True(derivedImmutable == baseImmutable.As()); + Assert.True(derivedImmutable == baseImmutable.CastArray()); } [Fact] @@ -332,13 +419,13 @@ public void CastUpReferenceDefaultValue() } [Fact] - public void CastUpRefToInterface() + public void CastUpReferenceToInterface() { var stringArray = ImmutableArray.Create("a", "b"); var enumArray = ImmutableArray.CastUp(stringArray); Assert.Equal(2, enumArray.Length); - Assert.Equal(stringArray, enumArray.CastArray()); - Assert.Equal(stringArray, enumArray.As()); + Assert.True(stringArray == enumArray.CastArray()); + Assert.True(stringArray == enumArray.As()); } [Fact] @@ -347,8 +434,8 @@ public void CastUpInterfaceToInterface() var genericEnumArray = ImmutableArray.Create>(new List(), new List()); var legacyEnumArray = ImmutableArray.CastUp(genericEnumArray); Assert.Equal(2, legacyEnumArray.Length); - Assert.Equal(genericEnumArray, legacyEnumArray.As>()); - Assert.Equal(genericEnumArray, legacyEnumArray.CastArray>()); + Assert.True(genericEnumArray == legacyEnumArray.As>()); + Assert.True(genericEnumArray == legacyEnumArray.CastArray>()); } [Fact] @@ -357,18 +444,18 @@ public void CastUpArrayToSystemArray() var arrayArray = ImmutableArray.Create(new int[] { 1, 2 }, new int[] { 3, 4 }); var sysArray = ImmutableArray.CastUp(arrayArray); Assert.Equal(2, sysArray.Length); - Assert.Equal(arrayArray, sysArray.As()); - Assert.Equal(arrayArray, sysArray.CastArray()); + Assert.True(arrayArray == sysArray.As()); + Assert.True(arrayArray == sysArray.CastArray()); } [Fact] public void CastUpArrayToObject() { var arrayArray = ImmutableArray.Create(new int[] { 1, 2 }, new int[] { 3, 4 }); - var objArray = ImmutableArray.CastUp(arrayArray); - Assert.Equal(2, objArray.Length); - Assert.Equal(arrayArray, objArray.As()); - Assert.Equal(arrayArray, objArray.CastArray()); + var objectArray = ImmutableArray.CastUp(arrayArray); + Assert.Equal(2, objectArray.Length); + Assert.True(arrayArray == objectArray.As()); + Assert.True(arrayArray == objectArray.CastArray()); } [Fact] @@ -377,19 +464,19 @@ public void CastUpDelegateToSystemDelegate() var delArray = ImmutableArray.Create(() => { }, () => { }); var sysDelArray = ImmutableArray.CastUp(delArray); Assert.Equal(2, sysDelArray.Length); - Assert.Equal(delArray, sysDelArray.As()); - Assert.Equal(delArray, sysDelArray.CastArray()); + Assert.True(delArray == sysDelArray.As()); + Assert.True(delArray == sysDelArray.CastArray()); } [Fact] public void CastArrayUnrelatedInterface() { - var strArray = ImmutableArray.Create("cat", "dog"); - var compArray = ImmutableArray.CastUp(strArray); - var enumArray = compArray.CastArray(); + var stringArray = ImmutableArray.Create("cat", "dog"); + var comparableArray = ImmutableArray.CastUp(stringArray); + var enumArray = comparableArray.CastArray(); Assert.Equal(2, enumArray.Length); - Assert.Equal(strArray, enumArray.As()); - Assert.Equal(strArray, enumArray.CastArray()); + Assert.True(stringArray == enumArray.As()); + Assert.True(stringArray == enumArray.CastArray()); } [Fact] @@ -400,48 +487,57 @@ public void CastArrayBadInterface() } [Fact] - public void CastArrayBadRef() + public void CastArrayBadReference() { - var objArray = ImmutableArray.Create("cat", "dog"); - Assert.Throws(() => objArray.CastArray()); + var objectArray = ImmutableArray.Create("cat", "dog"); + Assert.Throws(() => objectArray.CastArray()); } - [Fact] - public void ToImmutableArray() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void ToImmutableArray(IEnumerable source) { - IEnumerable source = new[] { 1, 2, 3 }; - ImmutableArray immutable = source.ToImmutableArray(); - Assert.Equal(source, immutable); + var array = source.ToImmutableArray(); + Assert.Equal(source, array); + Assert.True(array == array.ToImmutableArray()); // Compares referential equality. + } + + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void Count(IEnumerable source) + { + var array = source.ToImmutableArray(); - ImmutableArray immutable2 = immutable.ToImmutableArray(); - Assert.Equal(immutable, immutable2); // this will compare array reference equality. + Assert.Equal(source.Count(), array.Length); + Assert.Equal(source.Count(), ((ICollection)array).Count); + Assert.Equal(source.Count(), ((ICollection)array).Count); + Assert.Equal(source.Count(), ((IReadOnlyCollection)array).Count); } [Fact] - public void Count() + public void CountInvalid() { Assert.Throws(() => s_emptyDefault.Length); Assert.Throws(() => ((ICollection)s_emptyDefault).Count); Assert.Throws(() => ((ICollection)s_emptyDefault).Count); Assert.Throws(() => ((IReadOnlyCollection)s_emptyDefault).Count); + } - Assert.Equal(0, s_empty.Length); - Assert.Equal(0, ((IReadOnlyCollection)s_empty).Count); - - Assert.Equal(1, s_oneElement.Length); - Assert.Equal(1, ((IReadOnlyCollection)s_oneElement).Count); + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void IsEmpty(IEnumerable source) + { + Assert.Equal(!source.Any(), source.ToImmutableArray().IsEmpty); } [Fact] - public void IsEmpty() + public void IsEmptyInvalid() { Assert.Throws(() => s_emptyDefault.IsEmpty); - Assert.True(s_empty.IsEmpty); - Assert.False(s_oneElement.IsEmpty); } [Fact] - public void IndexOfDefault() + public void IndexOfInvalid() { Assert.Throws(() => s_emptyDefault.IndexOf(5)); Assert.Throws(() => s_emptyDefault.IndexOf(5, 0)); @@ -449,7 +545,7 @@ public void IndexOfDefault() } [Fact] - public void LastIndexOfDefault() + public void LastIndexOfInvalid() { Assert.Throws(() => s_emptyDefault.LastIndexOf(5)); Assert.Throws(() => s_emptyDefault.LastIndexOf(5, 0)); @@ -479,948 +575,1525 @@ public void LastIndexOf() (b, v, i, c, eq) => b.LastIndexOf(v, i, c, eq)); } - [Fact] - public void Contains() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void ContainsInt32(IEnumerable source) { - Assert.Throws(() => s_emptyDefault.Contains(0)); - Assert.False(s_empty.Contains(0)); - Assert.True(s_oneElement.Contains(1)); - Assert.False(s_oneElement.Contains(2)); - Assert.True(s_manyElements.Contains(3)); - Assert.False(s_oneElementRefType.Contains(null)); - Assert.True(s_twoElementRefTypeWithNull.Contains(null)); + var array = source.ToImmutableArray(); + + if (source.Any(i => i >= 0)) + { + int contained = Enumerable.Range(0, int.MaxValue).First(i => source.Contains(i)); + Assert.True(array.Contains(contained)); + Assert.True(((ICollection)array).Contains(contained)); + } + + int notContained = Enumerable.Range(0, int.MaxValue).First(i => !source.Contains(i)); + Assert.False(array.Contains(notContained)); + Assert.False(((ICollection)array).Contains(notContained)); } - [Fact] - public void ContainsEqualityComparer() + [Theory] + [MemberData(nameof(ContainsNullData))] + public void ContainsNull(IEnumerable source, T dummy) where T : class { - var array = ImmutableArray.Create("a", "B"); - Assert.False(array.Contains("A", StringComparer.Ordinal)); - Assert.True(array.Contains("A", StringComparer.OrdinalIgnoreCase)); - Assert.False(array.Contains("b", StringComparer.Ordinal)); - Assert.True(array.Contains("b", StringComparer.OrdinalIgnoreCase)); + // Remove the dummy parameters once https://github.com/xunit/xunit/pull/965 makes it into corefx. + + bool expected = source.Contains(null, EqualityComparer.Default); + Assert.Equal(expected, source.ToImmutableArray().Contains(null)); } - [Fact] - public void Enumerator() + public static IEnumerable ContainsNullData() { - Assert.Throws(() => s_emptyDefault.GetEnumerator()); - - ImmutableArray.Enumerator enumerator = default(ImmutableArray.Enumerator); - Assert.Throws(() => enumerator.Current); - Assert.Throws(() => enumerator.MoveNext()); + yield return new object[] { s_oneElementRefType, new GenericParameterHelper() }; + yield return new object[] { s_twoElementRefTypeWithNull, string.Empty }; + yield return new object[] { new[] { new object() }, new object() }; + } - enumerator = s_empty.GetEnumerator(); - Assert.Throws(() => enumerator.Current); - Assert.False(enumerator.MoveNext()); + [Fact] + public void ContainsInvalid() + { + Assert.Throws(() => s_emptyDefault.Contains(0)); + } - enumerator = s_manyElements.GetEnumerator(); - Assert.Throws(() => enumerator.Current); + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void GetEnumerator(IEnumerable source) + { + var array = source.ToImmutableArray(); + var enumeratorStruct = array.GetEnumerator(); - Assert.True(enumerator.MoveNext()); - Assert.Equal(1, enumerator.Current); - Assert.True(enumerator.MoveNext()); - Assert.Equal(2, enumerator.Current); - Assert.True(enumerator.MoveNext()); - Assert.Equal(3, enumerator.Current); + Assert.IsType.Enumerator>(enumeratorStruct); + AssertNotAssignableFrom(enumeratorStruct); + AssertNotAssignableFrom(enumeratorStruct); + AssertNotAssignableFrom>(enumeratorStruct); - Assert.False(enumerator.MoveNext()); - Assert.Throws(() => enumerator.Current); - } + var set = new HashSet(); - [Fact] - public void ObjectEnumerator() - { - Assert.Throws(() => ((IEnumerable)s_emptyDefault).GetEnumerator()); + set.Add(((IEnumerable)array).GetEnumerator()); + set.Add(((IEnumerable)array).GetEnumerator()); - IEnumerator enumerator = ((IEnumerable)s_empty).GetEnumerator(); - Assert.Throws(() => enumerator.Current); - Assert.False(enumerator.MoveNext()); + set.Add(((IEnumerable)array).GetEnumerator()); + set.Add(((IEnumerable)array).GetEnumerator()); - enumerator = ((IEnumerable)s_manyElements).GetEnumerator(); - Assert.Throws(() => enumerator.Current); + int expected = array.IsEmpty ? 1 : 4; // Empty ImmutableArrays should cache their enumerators. + Assert.Equal(expected, set.Count); + Assert.DoesNotContain(null, set); - for (int i = 0; i < 2; i++) + Assert.All(set, enumerator => { - Assert.True(enumerator.MoveNext()); - Assert.Equal(1, enumerator.Current); - Assert.True(enumerator.MoveNext()); - Assert.Equal(2, enumerator.Current); - Assert.True(enumerator.MoveNext()); - Assert.Equal(3, enumerator.Current); - if (i == 0) - enumerator.Reset(); - } - - Assert.False(enumerator.MoveNext()); - Assert.Throws(() => enumerator.Current); + Assert.NotEqual(enumeratorStruct.GetType(), enumerator.GetType()); + Assert.Equal(set.First().GetType(), enumerator.GetType()); + }); } - [Fact] - public void EnumeratorWithNullValues() + private static void AssertNotAssignableFrom(object obj) { - var enumerationResult = System.Linq.Enumerable.ToArray(s_twoElementRefTypeWithNull); - Assert.Equal("1", enumerationResult[0]); - Assert.Null(enumerationResult[1]); + var typeInfo = obj.GetType().GetTypeInfo(); + Assert.False(typeof(T).GetTypeInfo().IsAssignableFrom(typeInfo)); } [Fact] - public void EqualityCheckComparesInternalArrayByReference() + public void GetEnumeratorObjectEmptyReturnsSingleton() { - var immutable1 = ImmutableArray.Create(1); - var immutable2 = ImmutableArray.Create(1); - Assert.NotEqual(immutable1, immutable2); - - Assert.True(immutable1.Equals(immutable1)); - Assert.True(immutable1.Equals((object)immutable1)); + var empty = (IEnumerable)s_empty; + Assert.Same(empty.GetEnumerator(), empty.GetEnumerator()); } [Fact] - public void EqualsObjectNull() + public void GetEnumeratorInvalid() { - Assert.False(s_empty.Equals((object)null)); + Assert.Throws(() => s_emptyDefault.GetEnumerator()); + Assert.Throws(() => ((IEnumerable)s_emptyDefault).GetEnumerator()); + Assert.Throws(() => ((IEnumerable)s_emptyDefault).GetEnumerator()); } - [Fact] - public void OperatorsAndEquality() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void EnumeratorTraversal(IEnumerable source) { - Assert.True(s_empty.Equals(s_empty)); - var emptySame = s_empty; - Assert.True(s_empty == emptySame); - Assert.False(s_empty != emptySame); + var array = source.ToImmutableArray(); - // empty and default should not be seen as equal - Assert.False(s_empty.Equals(s_emptyDefault)); - Assert.False(s_empty == s_emptyDefault); - Assert.True(s_empty != s_emptyDefault); - Assert.False(s_emptyDefault == s_empty); - Assert.True(s_emptyDefault != s_empty); + var enumeratorStruct = array.GetEnumerator(); + var enumeratorObject = ((IEnumerable)array).GetEnumerator(); - Assert.False(s_empty.Equals(s_oneElement)); - Assert.False(s_empty == s_oneElement); - Assert.True(s_empty != s_oneElement); - Assert.False(s_oneElement == s_empty); - Assert.True(s_oneElement != s_empty); - } + Assert.Throws(() => enumeratorStruct.Current); + Assert.Throws(() => enumeratorObject.Current); - [Fact] - public void NullableOperators() - { - ImmutableArray? nullArray = null; - ImmutableArray? nonNullDefault = s_emptyDefault; - ImmutableArray? nonNullEmpty = s_empty; + int count = source.Count(); - Assert.True(nullArray == nonNullDefault); - Assert.False(nullArray != nonNullDefault); - Assert.True(nonNullDefault == nullArray); - Assert.False(nonNullDefault != nullArray); + for (int i = 0; i < count; i++) + { + Assert.True(enumeratorStruct.MoveNext()); + Assert.True(enumeratorObject.MoveNext()); - Assert.False(nullArray == nonNullEmpty); - Assert.True(nullArray != nonNullEmpty); - Assert.False(nonNullEmpty == nullArray); - Assert.True(nonNullEmpty != nullArray); - } + int element = source.ElementAt(i); + Assert.Equal(element, enumeratorStruct.Current); + Assert.Equal(element, enumeratorObject.Current); + Assert.Equal(element, ((IEnumerator)enumeratorObject).Current); + } - [Fact] - public void GetHashCodeTest() - { - Assert.Equal(0, s_emptyDefault.GetHashCode()); - Assert.NotEqual(0, s_empty.GetHashCode()); - Assert.NotEqual(0, s_oneElement.GetHashCode()); - } + Assert.False(enumeratorStruct.MoveNext()); + Assert.False(enumeratorObject.MoveNext()); - [Fact] - public void Add() - { - var source = new[] { 1, 2 }; - var array1 = ImmutableArray.Create(source); - var array2 = array1.Add(3); - Assert.Equal(source, array1); - Assert.Equal(new[] { 1, 2, 3 }, array2); - Assert.Equal(new[] { 1 }, s_empty.Add(1)); + Assert.Throws(() => enumeratorStruct.Current); + Assert.Throws(() => enumeratorObject.Current); } - [Fact] - public void AddRange() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void EnumeratorObjectTraversalDisposeReset(IEnumerable source) { - var nothingToEmpty = s_empty.AddRange(Enumerable.Empty()); - Assert.False(nothingToEmpty.IsDefault); - Assert.True(nothingToEmpty.IsEmpty); + var array = (IEnumerable)source.ToImmutableArray(); + var enumerator = array.GetEnumerator(); - Assert.Equal(new[] { 1, 2 }, s_empty.AddRange(Enumerable.Range(1, 2))); - Assert.Equal(new[] { 1, 2 }, s_empty.AddRange(new[] { 1, 2 })); + Assert.All(Enumerable.Range(0, source.Count()), bound => + { + enumerator.Reset(); + enumerator.Dispose(); // This should have no effect. - Assert.Equal(new[] { 1, 2, 3, 4 }, s_manyElements.AddRange(new[] { 4 })); - Assert.Equal(new[] { 1, 2, 3, 4, 5 }, s_manyElements.AddRange(new[] { 4, 5 })); + for (int i = 0; i < bound; i++) + { + int element = source.ElementAt(i); - Assert.Equal(new[] { 1, 2, 3, 4 }, s_manyElements.AddRange(ImmutableArray.Create(4))); - Assert.Equal(new[] { 1, 2, 3, 4, 5 }, s_manyElements.AddRange(ImmutableArray.Create(4, 5))); + enumerator.Dispose(); // This should have no effect. + Assert.True(enumerator.MoveNext()); + Assert.Equal(element, enumerator.Current); + Assert.Equal(element, ((IEnumerator)enumerator).Current); + } + }); } [Fact] - public void AddRangeDefaultEnumerable() + public void EnumeratorStructTraversalDefaultInvalid() { - Assert.Throws(() => s_emptyDefault.AddRange(Enumerable.Empty())); - Assert.Throws(() => s_emptyDefault.AddRange(Enumerable.Range(1, 2))); - Assert.Throws(() => s_emptyDefault.AddRange(new[] { 1, 2 })); + var enumerator = default(ImmutableArray.Enumerator); + Assert.Throws(() => enumerator.Current); + Assert.Throws(() => enumerator.MoveNext()); } - [Fact] - public void AddRangeDefaultStruct() + [Theory] + [MemberData(nameof(EnumeratorTraversalNullData))] + public void EnumeratorTraversalNull(IEnumerable source, T dummy) where T : class { - Assert.Throws(() => s_emptyDefault.AddRange(s_empty)); - Assert.Throws(() => s_empty.AddRange(s_emptyDefault)); - Assert.Throws(() => s_emptyDefault.AddRange(s_oneElement)); - Assert.Throws(() => s_oneElement.AddRange(s_emptyDefault)); + // Remove the dummy parameters once https://github.com/xunit/xunit/pull/965 makes it into corefx. - IEnumerable emptyBoxed = s_empty; - IEnumerable emptyDefaultBoxed = s_emptyDefault; - IEnumerable oneElementBoxed = s_oneElement; - Assert.Throws(() => s_emptyDefault.AddRange(emptyBoxed)); - Assert.Throws(() => s_empty.AddRange(emptyDefaultBoxed)); - Assert.Throws(() => s_emptyDefault.AddRange(oneElementBoxed)); - Assert.Throws(() => s_oneElement.AddRange(emptyDefaultBoxed)); + var array = ForceLazy(source.ToImmutableArray()).ToArray(); + Assert.Equal(source, array); + Assert.Contains(null, array); } - [Fact] - public void AddRangeNoOpIdentity() + public static IEnumerable EnumeratorTraversalNullData() { - Assert.Equal(s_empty, s_empty.AddRange(s_empty)); - Assert.Equal(s_oneElement, s_empty.AddRange(s_oneElement)); // struct overload - Assert.Equal(s_oneElement, s_empty.AddRange((IEnumerable)s_oneElement)); // enumerable overload - Assert.Equal(s_oneElement, s_oneElement.AddRange(s_empty)); + yield return new object[] { s_twoElementRefTypeWithNull, string.Empty }; + yield return new object[] { new[] { default(object) }, new object() }; + yield return new object[] { new[] { null, new object() }, new object() }; + yield return new object[] { new[] { null, string.Empty }, string.Empty }; } - [Fact] - public void Insert() + [Theory] + [MemberData(nameof(EqualsData))] + public void Equals(StrongBox> firstBox, StrongBox> secondBox, bool expected) { - var array1 = ImmutableArray.Create(); - Assert.Throws("index", () => array1.Insert(-1, 'a')); - Assert.Throws("index", () => array1.Insert(1, 'a')); + ImmutableArray first = firstBox.Value; + ImmutableArray second = secondBox.Value; - var insertFirst = array1.Insert(0, 'c'); - Assert.Equal(new[] { 'c' }, insertFirst); + Assert.Equal(expected, first == second); + Assert.NotEqual(expected, first != second); - var insertLeft = insertFirst.Insert(0, 'a'); - Assert.Equal(new[] { 'a', 'c' }, insertLeft); + Assert.Equal(expected, first.Equals(second)); + Assert.Equal(expected, AsEquatable(first).Equals(second)); + Assert.Equal(expected, first.Equals((object)second)); - var insertRight = insertFirst.Insert(1, 'e'); - Assert.Equal(new[] { 'c', 'e' }, insertRight); + Assert.Equal(expected, second == first); + Assert.NotEqual(expected, second != first); - var insertBetween = insertLeft.Insert(1, 'b'); - Assert.Equal(new[] { 'a', 'b', 'c' }, insertBetween); + Assert.Equal(expected, second.Equals(first)); + Assert.Equal(expected, AsEquatable(second).Equals(first)); + Assert.Equal(expected, second.Equals((object)first)); } - [Fact] - public void InsertDefault() + public static IEnumerable EqualsData() { - Assert.Throws(() => s_emptyDefault.Insert(-1, 10)); - Assert.Throws(() => s_emptyDefault.Insert(1, 10)); - Assert.Throws(() => s_emptyDefault.Insert(0, 10)); - } + // Once https://github.com/xunit/assert.xunit/pull/5 comes into corefx, all the StrongBox stuff can be removed. - [Fact] - public void InsertRangeEmptyInvalid() - { - Assert.Throws(() => s_emptyDefault.Insert(-1, 10)); - Assert.Throws(() => s_emptyDefault.Insert(1, 10)); - Assert.Throws("index", () => s_empty.InsertRange(1, s_oneElement)); - Assert.Throws("index", () => s_empty.InsertRange(-1, s_oneElement)); - Assert.Throws("index", () => s_empty.InsertRange(1, (IEnumerable)s_oneElement)); - Assert.Throws("index", () => s_empty.InsertRange(-1, (IEnumerable)s_oneElement)); - } + var enumerables = Int32EnumerableData() + .Select(array => array[0]) + .Cast>(); - [Fact] - public void InsertRangeDefault() - { - Assert.Throws(() => s_emptyDefault.InsertRange(1, Enumerable.Empty())); - Assert.Throws(() => s_emptyDefault.InsertRange(-1, Enumerable.Empty())); - Assert.Throws(() => s_emptyDefault.InsertRange(0, Enumerable.Empty())); - Assert.Throws(() => s_emptyDefault.InsertRange(0, new[] { 1 })); - Assert.Throws(() => s_emptyDefault.InsertRange(0, new[] { 2, 3, 4 })); - Assert.Throws(() => s_emptyDefault.InsertRange(0, Enumerable.Range(2, 3))); - } + foreach (var enumerable in enumerables) + { + var array = enumerable.ToImmutableArray(); - /// - /// Validates that a fixed bug in the inappropriate adding of the - /// Empty singleton enumerator to the reusable instances bag does not regress. - /// - [Fact] - public void EmptyEnumeratorReuseRegressionTest() - { - IEnumerable oneElementBoxed = s_oneElement; - IEnumerable emptyBoxed = s_empty; - IEnumerable emptyDefaultBoxed = s_emptyDefault; + yield return new object[] + { + new StrongBox>(array), + new StrongBox>(array), + true + }; - Assert.Throws(() => s_emptyDefault.RemoveRange(emptyBoxed)); - Assert.Throws(() => s_emptyDefault.RemoveRange(emptyDefaultBoxed)); - Assert.Throws(() => s_empty.RemoveRange(emptyDefaultBoxed)); - Assert.Equal(oneElementBoxed, oneElementBoxed); - } + // Reference equality, not content equality, should be compared. + yield return new object[] + { + new StrongBox>(array), + new StrongBox>(enumerable.ToImmutableArray()), + !enumerable.Any() || enumerable is ImmutableArray + }; + } - [Fact] - public void InsertRangeDefaultStruct() - { - Assert.Throws(() => s_emptyDefault.InsertRange(0, s_empty)); - Assert.Throws(() => s_empty.InsertRange(0, s_emptyDefault)); - Assert.Throws(() => s_emptyDefault.InsertRange(0, s_oneElement)); - Assert.Throws(() => s_oneElement.InsertRange(0, s_emptyDefault)); + // Empty and default ImmutableArrays should not be seen as equal. + yield return new object[] + { + new StrongBox>(s_empty), + new StrongBox>(s_emptyDefault), + false + }; - IEnumerable emptyBoxed = s_empty; - IEnumerable emptyDefaultBoxed = s_emptyDefault; - IEnumerable oneElementBoxed = s_oneElement; - Assert.Throws(() => s_emptyDefault.InsertRange(0, emptyBoxed)); - Assert.Throws(() => s_empty.InsertRange(0, emptyDefaultBoxed)); - Assert.Throws(() => s_emptyDefault.InsertRange(0, oneElementBoxed)); - Assert.Throws(() => s_oneElement.InsertRange(0, emptyDefaultBoxed)); + yield return new object[] + { + new StrongBox>(s_empty), + new StrongBox>(s_oneElement), + false + }; } - public static IEnumerable InsertRangeLeft() + [Theory] + [MemberData(nameof(StrongBoxedInt32EnumerableData))] + [MemberData(nameof(StrongBoxedSpecialInt32ImmutableArrayData))] + public void EqualsSelf(StrongBox> box) { - yield return new object[] { new[] { 7, 1, 2, 3 }, s_manyElements, 0, new[] { 7 } }; - yield return new object[] { new[] { 7, 8, 1, 2, 3 }, s_manyElements, 0, new[] { 7, 8 } }; - } + IEnumerable source = box.Value; + var array = source.ToImmutableArray(); - public static IEnumerable InsertRangeMiddle() - { - yield return new object[] { new[] { 1, 7, 2, 3 }, s_manyElements, 1, new[] { 7 } }; - yield return new object[] { new[] { 1, 7, 8, 2, 3 }, s_manyElements, 1, new[] { 7, 8 } }; - } +#pragma warning disable CS1718 // Comparison made to same variable + Assert.True(array == array); + Assert.False(array != array); +#pragma warning restore CS1718 // Comparison made to same variable - public static IEnumerable InsertRangeRight() - { - yield return new object[] { new[] { 1, 2, 3, 7 }, s_manyElements, 3, new[] { 7 } }; - yield return new object[] { new[] { 1, 2, 3, 7, 8 }, s_manyElements, 3, new[] { 7, 8 } }; + Assert.True(array.Equals(array)); + Assert.True(AsEquatable(array).Equals(array)); + Assert.True(array.Equals((object)array)); } - public static IEnumerable InsertRangeEmpty() + [Theory] + [MemberData(nameof(StrongBoxedInt32EnumerableData))] + [MemberData(nameof(StrongBoxedSpecialInt32ImmutableArrayData))] + public void EqualsNull(StrongBox> box) { - yield return new object[] { new int[0], s_empty, 0, Enumerable.Empty() }; - yield return new object[] { s_empty, s_empty, 0, Enumerable.Empty() }; - yield return new object[] { new[] { 1 }, s_empty, 0, new[] { 1 } }; - yield return new object[] { new[] { 2, 3, 4 }, s_empty, 0, new[] { 2, 3, 4 } }; - yield return new object[] { new[] { 2, 3, 4 }, s_empty, 0, Enumerable.Range(2, 3) }; - yield return new object[] { s_manyElements, s_manyElements, 0, Enumerable.Empty() }; + IEnumerable source = box.Value; + Assert.False(source.ToImmutableArray().Equals(null)); } - public static IEnumerable InsertRangeIdentity() + [Theory] + [MemberData(nameof(StrongBoxedInt32EnumerableData))] + [MemberData(nameof(StrongBoxedSpecialInt32ImmutableArrayData))] + public void EqualsNullable(StrongBox> box) { - yield return new object[] { s_empty, s_empty, 0, s_empty }; - yield return new object[] { s_oneElement, s_empty, 0, s_oneElement }; - yield return new object[] { s_oneElement, s_oneElement, 0, s_empty }; + // ImmutableArray overrides the equality operators for ImmutableArray?. + // If one nullable with HasValue = false is compared to a nullable with HasValue = true, + // but Value.IsDefault = true, the nullables will compare as equal. + + IEnumerable source = box.Value; + var array = source.ToImmutableArray(); + ImmutableArray? nullable = array; + + Assert.Equal(array.IsDefault, null == nullable); + Assert.NotEqual(array.IsDefault, null != nullable); + + Assert.Equal(array.IsDefault, nullable == null); + Assert.NotEqual(array.IsDefault, nullable != null); } - public static IEnumerable InsertRangeDifferentUnderlyingType() + [Theory] + [MemberData(nameof(StrongBoxedInt32EnumerableData))] + [MemberData(nameof(StrongBoxedSpecialInt32ImmutableArrayData))] + public void GetHashCode(StrongBox> box) { - yield return new object[] { new int[] { 1, 2, 3 }, s_empty, 0, (int[])(object)new uint[] { 1, 2, 3 } }; - yield return new object[] { new int[] { 4, 5, 6, 1, 2, 3 }, s_manyElements, 0, (int[])(object)new uint[] { 4, 5, 6 } }; - yield return new object[] { new int[] { 1, 2, 3, 4, 5, 6 }, s_manyElements, 3, (int[])(object)new uint[] { 4, 5, 6 } }; + IEnumerable source = box.Value; + var array = source.ToImmutableArray(); + + // We must box once. Otherwise, the following assert would not have much purpose since + // RuntimeHelpers.GetHashCode returns different values for boxed objects that are not + // reference-equal. + object boxed = array; + + // The default implementation of object.GetHashCode is a call to RuntimeHelpers.GetHashCode. + // This assert effectively ensures that ImmutableArray overrides GetHashCode. + Assert.NotEqual(RuntimeHelpers.GetHashCode(boxed), boxed.GetHashCode()); + + // Ensure that the hash is consistent. + Assert.Equal(array.GetHashCode(), array.GetHashCode()); + + if (array.IsDefault) + { + Assert.Equal(0, array.GetHashCode()); + } + else if (array.IsEmpty) + { + // Empty array instances should be cached. + var same = ImmutableArray.Create(new int[0]); + Assert.Equal(array.GetHashCode(), same.GetHashCode()); + } + + // Use reflection to retrieve the underlying array, and ensure that the ImmutableArray's + // hash code is equivalent to the array's hash code. + + int[] underlyingArray = GetUnderlyingArray(array); + Assert.Equal(underlyingArray?.GetHashCode() ?? 0, array.GetHashCode()); } [Theory] - [MemberData(nameof(InsertRangeLeft))] - [MemberData(nameof(InsertRangeMiddle))] - [MemberData(nameof(InsertRangeRight))] - [MemberData(nameof(InsertRangeEmpty))] - [MemberData(nameof(InsertRangeIdentity))] - public void InsertRange(IEnumerable expected, ImmutableArray array, int index, IEnumerable items) + [MemberData(nameof(AddData))] + public void Add(IEnumerable source, IEnumerable items) { - // All of these functions should take an enumerable and produce another w/ the same contents. - var identityTransforms = new List, IEnumerable>> - { - e => e, - e => e.ToArray(), // Array - e => e.ToList(), // List - e => new LinkedList(e), // Non-array, non-List, non-ImmutableArray IList - e => e.ToImmutableArray(), // ImmutableArray - e => e.Select(i => i), // Lazy enumerable - e => new Queue(e) // IReadOnlyCollection / non-generic ICollection - }; + var array = source.ToImmutableArray(); - foreach (var equivalentItems in identityTransforms.Select(t => t(items))) + var list = new List>>(); + + int index = 0; + foreach (int item in items) { - Assert.Equal(expected, array.InsertRange(index, equivalentItems)); - Assert.Equal(expected, array.InsertRange(index, equivalentItems.ToImmutableArray())); // Call the ImmutableArray overload. + // Take a snapshot of the ImmutableArray before the Add. + list.Add(Tuple.Create(array.ToArray(), array)); + + // Add the next item. + array = array.Add(item); - if (index == array.Length) // Insertion @ the end should be equivalent to adding. + var expected = source.Concat(items.Take(++index)); + Assert.Equal(expected, array); + + // Go back to previous ImmutableArrays and make sure their contents + // didn't change by comparing them against their snapshots. + foreach (var tuple in list) { - Assert.Equal(expected, array.AddRange(equivalentItems)); - Assert.Equal(expected, array.AddRange(equivalentItems.ToImmutableArray())); + Assert.Equal(tuple.Item1, tuple.Item2); } } } - [Fact] - public void RemoveAt() + [Theory] + [MemberData(nameof(AddData))] + public void AddRange(IEnumerable source, IEnumerable items) { - Assert.Throws("length", () => s_empty.RemoveAt(0)); - Assert.Throws(() => s_emptyDefault.RemoveAt(0)); - Assert.Throws("length", () => s_oneElement.RemoveAt(1)); - Assert.Throws("index", () => s_empty.RemoveAt(-1)); + Assert.All(ChangeType(items), it => + { + var array = source.ToImmutableArray(); - Assert.Equal(new int[0], s_oneElement.RemoveAt(0)); - Assert.Equal(new[] { 2, 3 }, s_manyElements.RemoveAt(0)); - Assert.Equal(new[] { 1, 3 }, s_manyElements.RemoveAt(1)); - Assert.Equal(new[] { 1, 2 }, s_manyElements.RemoveAt(2)); + Assert.Equal(source.Concat(items), array.AddRange(it)); // Enumerable overload + Assert.Equal(source.Concat(items), array.AddRange(it.ToImmutableArray())); // Struct overload + Assert.Equal(source, array); // Make sure the original array wasn't affected. + }); } - [Fact] - public void Remove_NullEqualityComparer() + public static IEnumerable AddData() { - var modified = s_manyElements.Remove(2, null); - Assert.Equal(new[] { 1, 3 }, modified); - - // Try again through the explicit interface implementation. - IImmutableList boxedCollection = s_manyElements; - var modified2 = boxedCollection.Remove(2, null); - Assert.Equal(new[] { 1, 3 }, modified2); + yield return new object[] { new int[] { }, new[] { 1 } }; + yield return new object[] { new[] { 1, 2 }, new[] { 3 } }; + yield return new object[] { s_empty, Enumerable.Empty() }; + yield return new object[] { s_empty, Enumerable.Range(1, 2) }; + yield return new object[] { s_empty, new[] { 1, 2 } }; + yield return new object[] { s_manyElements, new[] { 4 } }; + yield return new object[] { s_manyElements, new[] { 4, 5 } }; + yield return new object[] { s_manyElements, new[] { 4 } }; + yield return new object[] { s_manyElements, new[] { 4, 5 } }; + yield return new object[] { s_empty, s_empty }; + yield return new object[] { s_empty, s_oneElement }; + yield return new object[] { s_oneElement, s_empty }; } - [Fact] - public void Remove() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void AddRangeInvalid(IEnumerable source) { - Assert.Throws(() => s_emptyDefault.Remove(5)); - Assert.False(s_empty.Remove(5).IsDefault); + // If the lhs or the rhs is a default ImmutableArray, AddRange should throw. - Assert.True(s_oneElement.Remove(1).IsEmpty); - Assert.Equal(new[] { 2, 3 }, s_manyElements.Remove(1)); - Assert.Equal(new[] { 1, 3 }, s_manyElements.Remove(2)); - Assert.Equal(new[] { 1, 2 }, s_manyElements.Remove(3)); - } + Assert.Throws(() => s_emptyDefault.AddRange(source)); // Enumerable overload + Assert.Throws(() => s_emptyDefault.AddRange(source.ToImmutableArray())); // Struct overload + Assert.Throws(() => source.ToImmutableArray().AddRange(s_emptyDefault)); // Struct overload + Assert.Throws(() => source.ToImmutableArray().AddRange((IEnumerable)s_emptyDefault)); // Enumerable overload - [Fact] - public void RemoveRange() - { - Assert.Equal(s_empty, s_empty.RemoveRange(0, 0)); - Assert.Throws(() => s_emptyDefault.RemoveRange(0, 0)); - Assert.Throws("index", () => s_emptyDefault.RemoveRange(-1, 0)); - Assert.Throws(() => s_emptyDefault.RemoveRange(0, -1)); - Assert.Throws("index", () => s_oneElement.RemoveRange(2, 0)); - Assert.Equal(s_oneElement, s_oneElement.RemoveRange(1, 0)); - Assert.Throws("index", () => s_empty.RemoveRange(-1, 0)); - Assert.Throws("length", () => s_oneElement.RemoveRange(0, 2)); - Assert.Throws("length", () => s_oneElement.RemoveRange(0, -1)); - - var fourElements = ImmutableArray.Create(1, 2, 3, 4); - Assert.Equal(new int[0], s_oneElement.RemoveRange(0, 1)); - Assert.Equal(s_oneElement.ToArray(), s_oneElement.RemoveRange(0, 0)); - Assert.Equal(new[] { 3, 4 }, fourElements.RemoveRange(0, 2)); - Assert.Equal(new[] { 1, 4 }, fourElements.RemoveRange(1, 2)); - Assert.Equal(new[] { 1, 2 }, fourElements.RemoveRange(2, 2)); + Assert.Throws(() => s_emptyDefault.AddRange(s_emptyDefault)); + Assert.Throws(() => s_emptyDefault.AddRange((IEnumerable)s_emptyDefault)); } - [Fact] - public void RemoveRangeDefaultStruct() + [Theory] + [MemberData(nameof(InsertData))] + public void Insert(IEnumerable source, int index, T item) { - Assert.Throws(() => s_emptyDefault.RemoveRange(s_empty)); - Assert.Throws("items", () => s_empty.RemoveRange(s_emptyDefault)); - Assert.Throws(() => s_emptyDefault.RemoveRange(s_oneElement)); - Assert.Throws("items", () => s_oneElement.RemoveRange(s_emptyDefault)); + var expected = source.Take(index) + .Concat(new[] { item }) + .Concat(source.Skip(index)); + var array = source.ToImmutableArray(); - IEnumerable emptyBoxed = s_empty; - IEnumerable emptyDefaultBoxed = s_emptyDefault; - IEnumerable oneElementBoxed = s_oneElement; - Assert.Throws(() => s_emptyDefault.RemoveRange(emptyBoxed)); - Assert.Throws(() => s_empty.RemoveRange(emptyDefaultBoxed)); - Assert.Throws(() => s_emptyDefault.RemoveRange(oneElementBoxed)); - Assert.Throws(() => s_oneElement.RemoveRange(emptyDefaultBoxed)); + Assert.Equal(expected, array.Insert(index, item)); + Assert.Equal(source, array); // Make sure the original array wasn't affected. } - [Fact] - public void RemoveRangeNoOpIdentity() + public static IEnumerable InsertData() { - Assert.Equal(s_empty, s_empty.RemoveRange(s_empty)); - Assert.Equal(s_empty, s_empty.RemoveRange(s_oneElement)); // struct overload - Assert.Equal(s_empty, s_empty.RemoveRange((IEnumerable)s_oneElement)); // enumerable overload - Assert.Equal(s_oneElement, s_oneElement.RemoveRange(s_empty)); + yield return new object[] { new char[] { }, 0, 'c' }; + yield return new object[] { new[] { 'c' }, 0, 'a' }; + yield return new object[] { new[] { 'c' }, 1, 'e' }; + yield return new object[] { new[] { 'a', 'c' }, 1, 'b' }; } - [Fact] - public void RemoveAll() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void InsertInvalid(IEnumerable source) { - Assert.Throws("match", () => s_oneElement.RemoveAll(null)); - - var array = ImmutableArray.CreateRange(Enumerable.Range(1, 10)); - var removedEvens = array.RemoveAll(n => n % 2 == 0); - var removedOdds = array.RemoveAll(n => n % 2 == 1); - var removedAll = array.RemoveAll(n => true); - var removedNone = array.RemoveAll(n => false); - - Assert.Equal(new[] { 1, 3, 5, 7, 9 }, removedEvens); - Assert.Equal(new[] { 2, 4, 6, 8, 10 }, removedOdds); - Assert.True(removedAll.IsEmpty); - Assert.Equal(Enumerable.Range(1, 10), removedNone); + var array = source.ToImmutableArray(); - Assert.False(s_empty.RemoveAll(n => false).IsDefault); - Assert.Throws(() => s_emptyDefault.RemoveAll(n => false)); + Assert.Throws("index", () => array.Insert(-1, 0x61)); + Assert.Throws("index", () => array.Insert(array.Length + 1, 0x61)); } - [Fact] - public void RemoveRange_EnumerableEqualityComparer_AcceptsNullEQ() + [Theory] + [InlineData(-1)] + [InlineData(1)] + [InlineData(0)] + public void InsertDefaultInvalid(int index) { - var array = ImmutableArray.Create(1, 2, 3); - var removed2eq = array.RemoveRange(new[] { 2 }, null); - Assert.Equal(2, removed2eq.Length); - Assert.Equal(new[] { 1, 3 }, removed2eq); + Assert.Throws(() => s_emptyDefault.Insert(index, 10)); } - [Fact] - public void RemoveRangeEnumerableTest() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void InsertRangeInvalid(IEnumerable source) { - var list = ImmutableArray.Create(1, 2, 3); - Assert.Throws("items", () => list.RemoveRange(null)); - Assert.Throws(() => s_emptyDefault.RemoveRange(new int[0]).IsDefault); - Assert.False(s_empty.RemoveRange(new int[0]).IsDefault); + var array = source.ToImmutableArray(); - ImmutableArray removed2 = list.RemoveRange(new[] { 2 }); - Assert.Equal(2, removed2.Length); - Assert.Equal(new[] { 1, 3 }, removed2); + Assert.Throws("index", () => array.InsertRange(array.Length + 1, s_oneElement)); + Assert.Throws("index", () => array.InsertRange(-1, s_oneElement)); - ImmutableArray removed13 = list.RemoveRange(new[] { 1, 3, 5 }); - Assert.Equal(1, removed13.Length); - Assert.Equal(new[] { 2 }, removed13); + Assert.Throws("index", () => array.InsertRange(array.Length + 1, (IEnumerable)s_oneElement)); + Assert.Throws("index", () => array.InsertRange(-1, (IEnumerable)s_oneElement)); + } + + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void InsertRangeDefaultInvalid(IEnumerable items) + { + var array = items.ToImmutableArray(); + + Assert.Throws(() => s_emptyDefault.InsertRange(1, items)); + Assert.Throws(() => s_emptyDefault.InsertRange(-1, items)); + Assert.Throws(() => s_emptyDefault.InsertRange(0, items)); - Assert.Equal(new[] { 1, 3, 6, 8, 9 }, ImmutableArray.CreateRange(Enumerable.Range(1, 10)).RemoveRange(new[] { 2, 4, 5, 7, 10 })); - Assert.Equal(new[] { 3, 6, 8, 9 }, ImmutableArray.CreateRange(Enumerable.Range(1, 10)).RemoveRange(new[] { 1, 2, 4, 5, 7, 10 })); + Assert.Throws(() => s_emptyDefault.InsertRange(1, array)); + Assert.Throws(() => s_emptyDefault.InsertRange(-1, array)); + Assert.Throws(() => s_emptyDefault.InsertRange(0, array)); - Assert.Equal(list, list.RemoveRange(new[] { 5 })); - Assert.Equal(ImmutableArray.Create(), ImmutableArray.Create().RemoveRange(new[] { 1 })); + Assert.Throws(() => array.InsertRange(1, s_emptyDefault)); + Assert.Throws(() => array.InsertRange(-1, s_emptyDefault)); + Assert.Throws(() => array.InsertRange(0, s_emptyDefault)); - var listWithDuplicates = ImmutableArray.Create(1, 2, 2, 3); - Assert.Equal(new[] { 1, 2, 3 }, listWithDuplicates.RemoveRange(new[] { 2 })); - Assert.Equal(new[] { 1, 3 }, listWithDuplicates.RemoveRange(new[] { 2, 2 })); - Assert.Equal(new[] { 1, 3 }, listWithDuplicates.RemoveRange(new[] { 2, 2, 2 })); + if (array.Length > 0) + { + Assert.Throws(() => array.InsertRange(1, (IEnumerable)s_emptyDefault)); + } + + Assert.Throws(() => array.InsertRange(0, (IEnumerable)s_emptyDefault)); } - [Fact] - public void RemoveRangeImmutableArrayTest() + [Theory] + [MemberData(nameof(InsertRangeData))] + public void InsertRange(IEnumerable source, int index, IEnumerable items) { - var list = ImmutableArray.Create(1, 2, 3); + var array = source.ToImmutableArray(); - ImmutableArray removed2 = list.RemoveRange(ImmutableArray.Create(2)); - Assert.Equal(2, removed2.Length); - Assert.Equal(new[] { 1, 3 }, removed2); + Assert.All(ChangeType(items), it => + { + var expected = source.Take(index) + .Concat(items) + .Concat(source.Skip(index)); - ImmutableArray removed13 = list.RemoveRange(ImmutableArray.Create(1, 3, 5)); - Assert.Equal(1, removed13.Length); - Assert.Equal(new[] { 2 }, removed13); + Assert.Equal(expected, array.InsertRange(index, it)); // Enumerable overload + Assert.Equal(expected, array.InsertRange(index, it.ToImmutableArray())); // Struct overload - Assert.Equal(new[] { 1, 3, 6, 8, 9 }, ImmutableArray.CreateRange(Enumerable.Range(1, 10)).RemoveRange(ImmutableArray.Create(2, 4, 5, 7, 10))); - Assert.Equal(new[] { 3, 6, 8, 9 }, ImmutableArray.CreateRange(Enumerable.Range(1, 10)).RemoveRange(ImmutableArray.Create(1, 2, 4, 5, 7, 10))); + if (index == array.Length) + { + // Insertion at the end is equivalent to adding. + Assert.Equal(expected, array.InsertRange(index, it)); // Enumerable overload + Assert.Equal(expected, array.InsertRange(index, it.ToImmutableArray())); // Struct overload + } + }); + } - Assert.Equal(list, list.RemoveRange(ImmutableArray.Create(5))); - Assert.Equal(ImmutableArray.Create(), ImmutableArray.Create().RemoveRange(ImmutableArray.Create(1))); + public static IEnumerable InsertRangeData() + { + yield return new object[] { s_manyElements, 0, new[] { 7 } }; + yield return new object[] { s_manyElements, 0, new[] { 7, 8 } }; + yield return new object[] { s_manyElements, 1, new[] { 7 } }; + yield return new object[] { s_manyElements, 1, new[] { 7, 8 } }; + yield return new object[] { s_manyElements, 3, new[] { 7 } }; + yield return new object[] { s_manyElements, 3, new[] { 7, 8 } }; + yield return new object[] { s_empty, 0, new[] { 1 } }; + yield return new object[] { s_empty, 0, new[] { 2, 3, 4 } }; + yield return new object[] { s_manyElements, 0, new int[0] }; + yield return new object[] { s_empty, 0, s_empty }; + yield return new object[] { s_empty, 0, s_oneElement }; + yield return new object[] { s_oneElement, 0, s_empty }; + yield return new object[] { s_empty, 0, new uint[] { 1, 2, 3 } }; + yield return new object[] { s_manyElements, 0, new uint[] { 4, 5, 6 } }; + yield return new object[] { s_manyElements, 3, new uint[] { 4, 5, 6 } }; + } - var listWithDuplicates = ImmutableArray.Create(1, 2, 2, 3); - Assert.Equal(new[] { 1, 2, 3 }, listWithDuplicates.RemoveRange(ImmutableArray.Create(2))); - Assert.Equal(new[] { 1, 3 }, listWithDuplicates.RemoveRange(ImmutableArray.Create(2, 2))); - Assert.Equal(new[] { 1, 3 }, listWithDuplicates.RemoveRange(ImmutableArray.Create(2, 2, 2))); + [Theory] + [MemberData(nameof(RemoveAtData))] + public void RemoveAt(IEnumerable source, int index) + { + var array = source.ToImmutableArray(); + var expected = source.Take(index).Concat(source.Skip(index + 1)); + Assert.Equal(expected, array.RemoveAt(index)); + } - Assert.Equal(new[] { 2, 3 }, list.RemoveRange(ImmutableArray.Create(42), EverythingEqual.Default)); - Assert.Equal(new[] { 3 }, list.RemoveRange(ImmutableArray.Create(42, 42), EverythingEqual.Default)); - Assert.Equal(new int[0], list.RemoveRange(ImmutableArray.Create(42, 42, 42), EverythingEqual.Default)); + public static IEnumerable RemoveAtData() + { + yield return new object[] { s_oneElement, 0 }; + yield return new object[] { s_manyElements, 0 }; + yield return new object[] { s_manyElements, 1 }; + yield return new object[] { s_manyElements, 2 }; } - [Fact] - public void Replace() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void RemoveAtInvalid(IEnumerable source) { - Assert.Equal(new[] { 5 }, s_oneElement.Replace(1, 5)); + var array = source.ToImmutableArray(); - Assert.Equal(new[] { 6, 2, 3 }, s_manyElements.Replace(1, 6)); - Assert.Equal(new[] { 1, 6, 3 }, s_manyElements.Replace(2, 6)); - Assert.Equal(new[] { 1, 2, 6 }, s_manyElements.Replace(3, 6)); + Assert.Throws("index", () => array.RemoveAt(-1)); + Assert.Throws("length", () => array.RemoveAt(array.Length)); + Assert.Throws("index", () => array.RemoveAt(array.Length + 1)); + } - Assert.Equal(new[] { 1, 2, 3, 4 }, ImmutableArray.Create(1, 3, 3, 4).Replace(3, 2)); + [Theory] + [InlineData(-1, Skip = "#14961")] + [InlineData(0)] + [InlineData(1)] + public void RemoveAtDefaultInvalid(int index) + { + Assert.Throws(() => s_emptyDefault.RemoveAt(index)); } - [Fact] - public void ReplaceWithEqualityComparerTest() + [Theory] + [MemberData(nameof(RemoveData))] + public void Remove(IEnumerable source, T item, IEqualityComparer comparer) { - var updatedArray = s_manyElements.Replace(2, 10, null); - Assert.Equal(new[] { 1, 10, 3 }, updatedArray); + var array = source.ToImmutableArray(); - // Finally, try one last time using the interface implementation. - IImmutableList iface = s_manyElements; - var updatedIFace = iface.Replace(2, 10, null); - Assert.Equal(new[] { 1, 10, 3 }, updatedIFace); + var comparerOrDefault = comparer ?? EqualityComparer.Default; + var expected = source + .TakeWhile(x => !comparerOrDefault.Equals(x, item)) + .Concat(source.SkipWhile(x => !comparerOrDefault.Equals(x, item)).Skip(1)); + + Assert.Equal(expected, array.Remove(item, comparer)); + Assert.Equal(expected, ((IImmutableList)array).Remove(item, comparer)); + + if (comparer == null || comparer == EqualityComparer.Default) + { + Assert.Equal(expected, array.Remove(item)); + Assert.Equal(expected, ((IImmutableList)array).Remove(item)); + } } - [Fact] - public void ReplaceMissingThrowsTest() + public static IEnumerable RemoveData() { - Assert.Throws("oldValue", () => s_empty.Replace(5, 3)); + return SharedEqualityComparers().SelectMany(comparer => + new[] + { + new object[] { s_manyElements, 1, comparer }, + new object[] { s_manyElements, 2, comparer }, + new object[] { s_manyElements, 3, comparer }, + new object[] { s_manyElements, 4, comparer }, + new object[] { new int[0], 4, comparer }, + new object[] { new int[] { 1, 4 }, 4, comparer }, + new object[] { s_oneElement, 1, comparer } + }); } [Fact] - public void SetItem() + public void RemoveDefaultInvalid() + { + Assert.All(SharedEqualityComparers(), comparer => + { + Assert.Throws(() => s_emptyDefault.Remove(5)); + Assert.Throws(() => s_emptyDefault.Remove(5, comparer)); + + Assert.Throws(() => ((IImmutableList)s_emptyDefault).Remove(5)); + Assert.Throws(() => ((IImmutableList)s_emptyDefault).Remove(5, comparer)); + }); + } + + [Theory] + [MemberData(nameof(RemoveRangeIndexLengthData))] + public void RemoveRangeIndexLength(IEnumerable source, int index, int length) + { + var array = source.ToImmutableArray(); + var expected = source.Take(index).Concat(source.Skip(index + length)); + Assert.Equal(expected, array.RemoveRange(index, length)); + } + + public static IEnumerable RemoveRangeIndexLengthData() + { + yield return new object[] { s_empty, 0, 0 }; + yield return new object[] { s_oneElement, 1, 0 }; + yield return new object[] { s_oneElement, 0, 1 }; + yield return new object[] { s_oneElement, 0, 0 }; + yield return new object[] { new[] { 1, 2, 3, 4 }, 0, 2 }; + yield return new object[] { new[] { 1, 2, 3, 4 }, 1, 2 }; + yield return new object[] { new[] { 1, 2, 3, 4 }, 2, 2 }; + } + + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void RemoveRangeIndexLengthInvalid(IEnumerable source) + { + var array = source.ToImmutableArray(); + + Assert.Throws("index", () => array.RemoveRange(-1, 1)); + Assert.Throws("index", () => array.RemoveRange(array.Length + 1, 1)); + Assert.Throws("length", () => array.RemoveRange(0, -1)); + Assert.Throws("length", () => array.RemoveRange(0, array.Length + 1)); + } + + [Theory] + [InlineData(-1, 0, Skip = "#14961")] + [InlineData(0, -1)] + [InlineData(0, 0)] + [InlineData(1, -1)] + public void RemoveRangeIndexLengthDefaultInvalid(int index, int length) + { + Assert.Throws(() => s_emptyDefault.RemoveRange(index, length)); + } + + [Theory] + [MemberData(nameof(RemoveRangeEnumerableData))] + public void RemoveRangeEnumerable(IEnumerable source, IEnumerable items, IEqualityComparer comparer) { - Assert.Throws("index", () => s_empty.SetItem(0, 10)); - Assert.Throws(() => s_emptyDefault.SetItem(0, 10)); - Assert.Throws("index", () => s_oneElement.SetItem(1, 10)); - Assert.Throws("index", () => s_empty.SetItem(-1, 10)); + var array = source.ToImmutableArray(); + IEnumerable expected = items.Aggregate( + seed: source.ToImmutableArray(), + func: (a, i) => a.Remove(i, comparer)); + + Assert.Equal(expected, array.RemoveRange(items, comparer)); // Enumerable overload + Assert.Equal(expected, array.RemoveRange(items.ToImmutableArray(), comparer)); // Struct overload + Assert.Equal(expected, ((IImmutableList)array).RemoveRange(items, comparer)); - Assert.Equal(new[] { 12345 }, s_oneElement.SetItem(0, 12345)); - Assert.Equal(new[] { 12345, 2, 3 }, s_manyElements.SetItem(0, 12345)); - Assert.Equal(new[] { 1, 12345, 3 }, s_manyElements.SetItem(1, 12345)); - Assert.Equal(new[] { 1, 2, 12345 }, s_manyElements.SetItem(2, 12345)); + if (comparer == null || comparer == EqualityComparer.Default) + { + Assert.Equal(expected, array.RemoveRange(items)); // Enumerable overload + Assert.Equal(expected, array.RemoveRange(items.ToImmutableArray())); // Struct overload + Assert.Equal(expected, ((IImmutableList)array).RemoveRange(items)); + } + } + + public static IEnumerable RemoveRangeEnumerableData() + { + return SharedEqualityComparers().SelectMany(comparer => + new[] + { + new object[] { s_empty, s_empty, comparer }, + new object[] { s_empty, s_oneElement, comparer }, + new object[] { s_oneElement, s_empty, comparer }, + new object[] { new[] { 1, 2, 3 }, new[] { 2, 3, 4 }, comparer }, + new object[] { Enumerable.Range(1, 5), Enumerable.Range(6, 5), comparer }, + new object[] { new[] { 1, 2, 3 }, new[] { 2 }, comparer }, + new object[] { s_empty, new int[] { }, comparer }, + new object[] { new[] { 1, 2, 3 }, new[] { 2 }, comparer }, + new object[] { new[] { 1, 2, 3 }, new[] { 1, 3, 5 }, comparer }, + new object[] { Enumerable.Range(1, 10), new[] { 2, 4, 5, 7, 10 }, comparer }, + new object[] { Enumerable.Range(1, 10), new[] { 1, 2, 4, 5, 7, 10 }, comparer }, + new object[] { new[] { 1, 2, 3 }, new[] { 5 }, comparer }, + new object[] { new[] { 1, 2, 2, 3 }, new[] { 2 }, comparer }, + new object[] { new[] { 1, 2, 2, 3 }, new[] { 2, 2 }, comparer }, + new object[] { new[] { 1, 2, 2, 3 }, new[] { 2, 2, 2 }, comparer }, + new object[] { new[] { 1, 2, 3 }, new[] { 42 }, comparer }, + new object[] { new[] { 1, 2, 3 }, new[] { 42, 42 }, comparer }, + new object[] { new[] { 1, 2, 3 }, new[] { 42, 42, 42 }, comparer }, + }); + } + + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void RemoveRangeEnumerableInvalid(IEnumerable source) + { + var array = source.ToImmutableArray(); + + Assert.All(SharedEqualityComparers(), comparer => + { + // Enumerable overloads, lhs is default + Assert.Throws(() => s_emptyDefault.RemoveRange(source)); + Assert.Throws(() => s_emptyDefault.RemoveRange(source, comparer)); + Assert.Throws(() => ((IImmutableList)s_emptyDefault).RemoveRange(source)); + Assert.Throws(() => ((IImmutableList)s_emptyDefault).RemoveRange(source, comparer)); + + // Struct overloads, lhs is default + Assert.Throws(() => s_emptyDefault.RemoveRange(array)); + Assert.Throws(() => s_emptyDefault.RemoveRange(array, comparer)); + + // Struct overloads, rhs is default + Assert.Throws("items", () => array.RemoveRange(s_emptyDefault)); + Assert.Throws("items", () => array.RemoveRange(s_emptyDefault, comparer)); + + // Enumerable overloads, rhs is default + Assert.Throws(() => array.RemoveRange((IEnumerable)s_emptyDefault)); + Assert.Throws(() => array.RemoveRange((IEnumerable)s_emptyDefault, comparer)); + Assert.Throws(() => ((IImmutableList)array).RemoveRange(s_emptyDefault)); + Assert.Throws(() => ((IImmutableList)array).RemoveRange(s_emptyDefault, comparer)); + + // Struct overloads, both sides are default + Assert.Throws("items", () => s_emptyDefault.RemoveRange(s_emptyDefault)); + Assert.Throws("items", () => s_emptyDefault.RemoveRange(s_emptyDefault, comparer)); + + // Enumerable overloads, both sides are default + Assert.Throws(() => s_emptyDefault.RemoveRange((IEnumerable)s_emptyDefault)); + Assert.Throws(() => s_emptyDefault.RemoveRange((IEnumerable)s_emptyDefault, comparer)); + Assert.Throws(() => ((IImmutableList)s_emptyDefault).RemoveRange(s_emptyDefault)); + Assert.Throws(() => ((IImmutableList)s_emptyDefault).RemoveRange(s_emptyDefault, comparer)); + + // Enumerable overloads, rhs is null + Assert.Throws("items", () => array.RemoveRange(items: null)); + Assert.Throws("items", () => array.RemoveRange(items: null, equalityComparer: comparer)); + Assert.Throws("items", () => ((IImmutableList)array).RemoveRange(items: null)); + Assert.Throws("items", () => ((IImmutableList)array).RemoveRange(items: null, equalityComparer: comparer)); + + // Enumerable overloads, lhs is default and rhs is null + Assert.Throws(() => s_emptyDefault.RemoveRange(items: null)); + Assert.Throws(() => s_emptyDefault.RemoveRange(items: null, equalityComparer: comparer)); + Assert.Throws(() => ((IImmutableList)s_emptyDefault).RemoveRange(items: null)); + Assert.Throws(() => ((IImmutableList)s_emptyDefault).RemoveRange(items: null, equalityComparer: comparer)); + }); + } + + [Fact] + public void RemoveRangeEnumerableRegression() + { + // Validates that a fixed bug in the inappropriate adding of the Empty + // singleton enumerator to the reusable instances bag does not regress. + + IEnumerable oneElementBoxed = s_oneElement; + IEnumerable emptyBoxed = s_empty; + IEnumerable emptyDefaultBoxed = s_emptyDefault; + + Assert.Throws(() => s_emptyDefault.RemoveRange(emptyBoxed)); + Assert.Throws(() => s_emptyDefault.RemoveRange(emptyDefaultBoxed)); + Assert.Throws(() => s_empty.RemoveRange(emptyDefaultBoxed)); + + Assert.Equal(oneElementBoxed, oneElementBoxed); + } + + [Theory] + [MemberData(nameof(RemoveAllData))] + public void RemoveAll(IEnumerable source, Predicate match) + { + var array = source.ToImmutableArray(); + var expected = source.Where(i => !match(i)); + Assert.Equal(expected, array.RemoveAll(match)); + } + + public static IEnumerable RemoveAllData() + { + yield return new object[] { Enumerable.Range(1, 10), new Predicate(i => i % 2 == 0) }; + yield return new object[] { Enumerable.Range(1, 10), new Predicate(i => i % 2 == 1) }; + yield return new object[] { Enumerable.Range(1, 10), new Predicate(i => true) }; + yield return new object[] { Enumerable.Range(1, 10), new Predicate(i => false) }; + yield return new object[] { s_empty, new Predicate(i => false) }; + } + + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void RemoveAllInvalid(IEnumerable source) + { + var array = source.ToImmutableArray(); + + Assert.Throws("match", () => array.RemoveAll(match: null)); } [Fact] - public void CopyToArray() + public void RemoveAllDefaultInvalid() { + Assert.Throws(() => s_emptyDefault.RemoveAll(i => false)); + } + + [Theory] + [MemberData(nameof(ReplaceData))] + public void Replace(IEnumerable source, T oldValue, T newValue, IEqualityComparer comparer) + { + var array = source.ToImmutableArray(); + + var comparerOrDefault = comparer ?? EqualityComparer.Default; + var expected = source + .TakeWhile(x => !comparerOrDefault.Equals(x, oldValue)) + .Concat(new[] { newValue }) + .Concat(source.SkipWhile(x => !comparerOrDefault.Equals(x, oldValue)).Skip(1)); + + // If the comparer is a faulty implementation that says nothing is equal, + // an exception will be thrown here. Check that the comparer says the source contains + // this value first. + + if (source.Contains(oldValue, comparer)) { - var target = new int[s_manyElements.Length]; - s_manyElements.CopyTo(target); - Assert.Equal(target, s_manyElements); + Assert.Equal(expected, array.Replace(oldValue, newValue, comparer)); + Assert.Equal(expected, ((IImmutableList)array).Replace(oldValue, newValue, comparer)); } + if (comparer == null || comparer == EqualityComparer.Default) { - var target = new int[0]; - Assert.Throws(() => s_emptyDefault.CopyTo(target)); + Assert.Equal(expected, array.Replace(oldValue, newValue)); + Assert.Equal(expected, ((IImmutableList)array).Replace(oldValue, newValue)); } } - [Fact] - public void CopyToArrayInt() + public static IEnumerable ReplaceData() { - var source = ImmutableArray.Create(1, 2, 3); - var target = new int[4]; - source.CopyTo(target, 1); - Assert.Equal(new[] { 0, 1, 2, 3 }, target); + return SharedEqualityComparers().SelectMany(comparer => + new[] + { + new object[] { s_oneElement, 1, 5, comparer }, + new object[] { s_manyElements, 1, 6, comparer }, + new object[] { s_manyElements, 2, 6, comparer }, + new object[] { s_manyElements, 3, 6, comparer }, + new object[] { new[] { 1, 3, 3, 4 }, 3, 2, comparer }, + new object[] { s_manyElements, 2, 10, comparer } + }); + } + + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void ReplaceInvalid(IEnumerable source) + { + var array = source.ToImmutableArray(); + int notContained = Enumerable.Range(0, int.MaxValue).First(i => !source.Contains(i)); - Assert.Throws(() => s_emptyDefault.CopyTo(target, 0)); - Assert.Throws("dest", () => source.CopyTo(null, 0)); - Assert.Throws("dstIndex", () => source.CopyTo(target, -1)); - Assert.Throws("", () => source.CopyTo(target, 2)); + Assert.All(SharedEqualityComparers(), comparer => + { + Assert.Throws("oldValue", () => array.Replace(notContained, 123)); + Assert.Throws("oldValue", () => ((IImmutableList)array).Replace(notContained, 123)); + + // If the comparer is a faulty implementation that says everything is equal, + // an exception won't be thrown here. Check that the comparer says the source does + // not contain this value first. + if (!source.Contains(notContained, comparer)) + { + Assert.Throws("oldValue", () => array.Replace(notContained, 123, comparer)); + Assert.Throws("oldValue", () => ((IImmutableList)array).Replace(notContained, 123, comparer)); + } + }); } [Fact] - public void CopyToIntArrayIntInt() + public void ReplaceDefaultInvalid() { - var source = ImmutableArray.Create(1, 2, 3); - var target = new int[4]; - source.CopyTo(1, target, 3, 1); - Assert.Equal(new[] { 0, 0, 0, 2 }, target); + Assert.All(SharedEqualityComparers(), comparer => + { + // Uncomment when #14961 is fixed. + // Assert.Throws(() => s_emptyDefault.Replace(123, 123)); + // Assert.Throws(() => s_emptyDefault.Replace(123, 123, comparer)); + + Assert.Throws(() => ((IImmutableList)s_emptyDefault).Replace(123, 123)); + Assert.Throws(() => ((IImmutableList)s_emptyDefault).Replace(123, 123, comparer)); + }); } - [Fact] - public void Concat() + [Theory] + [MemberData(nameof(SetItemData))] + public void SetItem(IEnumerable source, int index, T item) { - var array1 = ImmutableArray.Create(1, 2, 3); - var array2 = ImmutableArray.Create(4, 5, 6); + var array = source.ToImmutableArray(); + var expected = source.ToArray(); + expected[index] = item; + Assert.Equal(expected, array.SetItem(index, item)); + } - var concat = array1.Concat(array2); - Assert.Equal(new[] { 1, 2, 3, 4, 5, 6 }, concat); + public static IEnumerable SetItemData() + { + yield return new object[] { s_oneElement, 0, 12345 }; + yield return new object[] { s_manyElements, 0, 12345 }; + yield return new object[] { s_manyElements, 1, 12345 }; + yield return new object[] { s_manyElements, 2, 12345 }; } - /// - /// Verifies reuse of the original array when concatenated to an empty array. - /// - [Fact] - public void ConcatEdgeCases() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void SetItemInvalid(IEnumerable source) { - // empty arrays - Assert.Equal(s_manyElements, s_manyElements.Concat(s_empty)); - Assert.Equal(s_manyElements, s_empty.Concat(s_manyElements)); + var array = source.ToImmutableArray(); - // default arrays - s_manyElements.Concat(s_emptyDefault); - Assert.Throws(() => s_manyElements.Concat(s_emptyDefault).Count()); - Assert.Throws(() => s_emptyDefault.Concat(s_manyElements).Count()); + Assert.Throws("index", () => array.SetItem(index: -1, item: 0)); + Assert.Throws("index", () => array.SetItem(index: array.Length, item: 0)); + Assert.Throws("index", () => array.SetItem(index: array.Length + 1, item: 0)); } - [Fact] - public void IsDefault() + [Theory] + [InlineData(-1, Skip = "#14961")] + [InlineData(0)] + [InlineData(1)] + public void SetItemDefaultInvalid(int index) { - Assert.True(s_emptyDefault.IsDefault); - Assert.False(s_empty.IsDefault); - Assert.False(s_oneElement.IsDefault); + Assert.Throws(() => s_emptyDefault.SetItem(index, item: 0)); } - [Fact] - public void IsDefaultOrEmpty() + [Theory] + [MemberData(nameof(CopyToData))] + public void CopyTo(IEnumerable source, int sourceIndex, IEnumerable destination, int destinationIndex, int length) { - Assert.True(s_empty.IsDefaultOrEmpty); - Assert.True(s_emptyDefault.IsDefaultOrEmpty); - Assert.False(s_oneElement.IsDefaultOrEmpty); + var array = source.ToImmutableArray(); + + // Take a snapshot of the destination array before calling CopyTo. + // Afterwards, ensure that the range we copied to was overwritten, and check + // that other areas were unaffected. + + CopyAndInvoke(destination, destinationArray => + { + array.CopyTo(sourceIndex, destinationArray, destinationIndex, length); + + Assert.Equal(destination.Take(destinationIndex), destinationArray.Take(destinationIndex)); + Assert.Equal(source.Skip(sourceIndex).Take(length), destinationArray.Skip(destinationIndex).Take(length)); + Assert.Equal(destination.Skip(destinationIndex + length), destinationArray.Skip(destinationIndex + length)); + }); + + if (sourceIndex == 0 && length == array.Length) + { + CopyAndInvoke(destination, destinationArray => + { + array.CopyTo(destinationArray, destinationIndex); + + Assert.Equal(destination.Take(destinationIndex), destinationArray.Take(destinationIndex)); + Assert.Equal(source, destinationArray.Skip(destinationIndex).Take(array.Length)); + Assert.Equal(destination.Skip(destinationIndex + array.Length), destinationArray.Skip(destinationIndex + array.Length)); + }); + + if (destinationIndex == 0) + { + CopyAndInvoke(destination, destinationArray => + { + array.CopyTo(destinationArray); + + Assert.Equal(source, destinationArray.Take(array.Length)); + Assert.Equal(destination.Skip(array.Length), destinationArray.Skip(array.Length)); + }); + } + } } - [Fact] - public void IndexGetter() + private static void CopyAndInvoke(IEnumerable source, Action action) => action(source.ToArray()); + + public static IEnumerable CopyToData() { - Assert.Equal(1, s_oneElement[0]); - Assert.Equal(1, ((IReadOnlyList)s_oneElement)[0]); + yield return new object[] { s_manyElements, 0, new int[3], 0, 3 }; + yield return new object[] { new[] { 1, 2, 3 }, 0, new int[4], 1, 3 }; + yield return new object[] { new[] { 1, 2, 3 }, 0, Enumerable.Range(1, 4), 1, 3 }; + yield return new object[] { new[] { 1, 2, 3 }, 1, new int[4], 3, 1 }; + yield return new object[] { new[] { 1, 2, 3 }, 1, Enumerable.Range(1, 4), 3, 1 }; + } + + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void CopyToInvalid(IEnumerable source) + { + var array = source.ToImmutableArray(); + + // ImmutableArray.CopyTo defers to Array.Copy for argument validation, so + // the parameter names here come from Array.Copy. - Assert.Throws(() => s_oneElement[1]); - Assert.Throws(() => s_oneElement[-1]); + Assert.Throws("dest", () => array.CopyTo(null)); + Assert.Throws("dest", () => array.CopyTo(null, 0)); + Assert.Throws("dest", () => array.CopyTo(0, null, 0, 0)); + Assert.Throws("dest", () => array.CopyTo(-1, null, -1, -1)); // The destination should be validated first. - Assert.Throws(() => s_emptyDefault[0]); - Assert.Throws(() => ((IList)s_emptyDefault)[0]); - Assert.Throws(() => ((IList)s_emptyDefault)[0]); - Assert.Throws(() => ((IReadOnlyList)s_emptyDefault)[0]); + Assert.Throws("length", () => array.CopyTo(-1, new int[0], -1, -1)); + Assert.Throws("srcIndex", () => array.CopyTo(-1, new int[0], -1, 0)); + Assert.Throws("dstIndex", () => array.CopyTo(0, new int[0], -1, 0)); + + Assert.Throws(() => array.CopyTo(array.Length, new int[1], 0, 1)); // Not enough room in the source. + + if (array.Length > 0) + { + Assert.Throws(() => array.CopyTo(array.Length - 1, new int[1], 1, 1)); // Not enough room in the destination. + } } - [Fact] - public void Sort() + [Theory] + [InlineData(0, 0)] + [InlineData(1, 0)] + [InlineData(2, 2)] + [InlineData(3, 1)] + public void CopyToDefaultInvalid(int destinationLength, int destinationIndex) { - var array = ImmutableArray.Create(2, 4, 1, 3); - Assert.Equal(new[] { 1, 2, 3, 4 }, array.Sort()); - Assert.Equal(new[] { 2, 4, 1, 3 }, array); // original array unaffected. + var destination = new int[destinationLength]; + + if (destinationIndex == 0) + { + Assert.Throws(() => s_emptyDefault.CopyTo(destination)); + } + + Assert.Throws(() => s_emptyDefault.CopyTo(destination, destinationIndex)); + Assert.Throws(() => s_emptyDefault.CopyTo(0, destination, destinationIndex, 0)); } [Theory] - [InlineData(new int[] { 2, 4, 1, 3 }, new int[] { 4, 3, 2, 1 })] - [InlineData(new int[] { 1 }, new int[] { 1 })] - [InlineData(new int[0], new int[0])] - public void Sort_Comparison(int[] items, int[] expected) + [MemberData(nameof(IsDefaultOrEmptyData))] + public void IsDefault(StrongBox> box, bool isDefault, bool isEmpty) { - var array = ImmutableArray.Create(items); - Assert.Equal(expected, array.Sort((x, y) => y.CompareTo(x))); - Assert.Equal(items, array); // original array unaffected. + IEnumerable source = box.Value; + var array = source.ToImmutableArray(); + + Assert.Equal(isDefault, array.IsDefault); } + [Theory] + [MemberData(nameof(IsDefaultOrEmptyData))] + public void IsDefaultOrEmpty(StrongBox> box, bool isDefault, bool isEmpty) + { + IEnumerable source = box.Value; + var array = source.ToImmutableArray(); - [Fact] - public void Sort_NullComparison_Throws() + Assert.Equal(isDefault || isEmpty, array.IsDefaultOrEmpty); + } + + public static IEnumerable IsDefaultOrEmptyData() { - Assert.Throws("comparison", () => ImmutableArray.Create().Sort((Comparison)null)); + // Once https://github.com/xunit/assert.xunit/pull/5 comes into corefx, all the StrongBox stuff can be removed. + + yield return new object[] { new StrongBox>(s_emptyDefault), true, false }; + yield return new object[] { new StrongBox>(s_empty), false, true }; + yield return new object[] { new StrongBox>(s_oneElement), false, false }; } - [Fact] - public void SortNullComparer() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void GetIndexer(IEnumerable source) { - var array = ImmutableArray.Create(2, 4, 1, 3); - Assert.Equal(new[] { 1, 2, 3, 4 }, array.Sort((IComparer)null)); - Assert.Equal(new[] { 2, 1, 4, 3 }, array.Sort(1, 2, null)); - Assert.Equal(new[] { 2, 4, 1, 3 }, array); // original array unaffected. + var array = source.ToImmutableArray(); + + for (int i = 0; i < array.Length; i++) + { + int expected = source.ElementAt(i); + + Assert.Equal(expected, array[i]); + Assert.Equal(expected, ((IList)array)[i]); + Assert.Equal(expected, ((IList)array)[i]); + Assert.Equal(expected, ((IReadOnlyList)array)[i]); + } } - [Fact] - public void SortRange() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void GetIndexerInvalid(IEnumerable source) { - var array = ImmutableArray.Create(2, 4, 1, 3); - Assert.Throws("index", () => array.Sort(-1, 2, Comparer.Default)); - Assert.Throws("count", () => array.Sort(1, -1, Comparer.Default)); - Assert.Throws("count", () => array.Sort(1, 4, Comparer.Default)); - Assert.Equal(new int[] { 2, 4, 1, 3 }, array.Sort(array.Length, 0, Comparer.Default)); - Assert.Equal(new[] { 2, 1, 4, 3 }, array.Sort(1, 2, Comparer.Default)); + var array = source.ToImmutableArray(); + + Assert.Throws(() => array[-1]); + Assert.Throws(() => ((IList)array)[-1]); + Assert.Throws(() => ((IList)array)[-1]); + Assert.Throws(() => ((IReadOnlyList)array)[-1]); + + Assert.Throws(() => array[array.Length]); + Assert.Throws(() => ((IList)array)[array.Length]); + Assert.Throws(() => ((IList)array)[array.Length]); + Assert.Throws(() => ((IReadOnlyList)array)[array.Length]); + + Assert.Throws(() => array[array.Length + 1]); + Assert.Throws(() => ((IList)array)[array.Length + 1]); + Assert.Throws(() => ((IList)array)[array.Length + 1]); + Assert.Throws(() => ((IReadOnlyList)array)[array.Length + 1]); } - [Fact] - public void SortComparer() + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public void GetIndexerDefaultInvalid(int index) { - var array = ImmutableArray.Create("c", "B", "a"); - Assert.Equal(new[] { "a", "B", "c" }, array.Sort(StringComparer.OrdinalIgnoreCase)); - Assert.Equal(new[] { "B", "a", "c" }, array.Sort(StringComparer.Ordinal)); + Assert.Throws(() => s_emptyDefault[index]); + Assert.Throws(() => ((IList)s_emptyDefault)[index]); + Assert.Throws(() => ((IList)s_emptyDefault)[index]); + Assert.Throws(() => ((IReadOnlyList)s_emptyDefault)[index]); } - [Fact] - public void SortPreservesArrayWhenAlreadySorted() + [Theory] + [MemberData(nameof(SortData))] + public void Sort(IEnumerable source, int index, int count, IComparer comparer, T dummy) { - var sortedArray = ImmutableArray.Create(1, 2, 3, 4); - Assert.Equal(sortedArray, sortedArray.Sort()); + // Remove the dummy parameters once https://github.com/xunit/xunit/pull/965 makes it into corefx. + + var array = source.ToImmutableArray(); + + var expected = source.ToArray(); + Array.Sort(expected, index, count, comparer); + + Assert.Equal(expected, array.Sort(index, count, comparer)); + Assert.Equal(source, array); // Make sure the original array is unaffected. + + if (index == 0 && count == array.Length) + { + Assert.Equal(expected, array.Sort(comparer)); + Assert.Equal(source, array); // Make sure the original array is unaffected. + + if (comparer != null) + { + Assert.Equal(expected, array.Sort(comparer.Compare)); + Assert.Equal(source, array); // Make sure the original array is unaffected. + } + + if (comparer == null || comparer == Comparer.Default) + { + Assert.Equal(expected, array.Sort()); + Assert.Equal(source, array); // Make sure the original array is unaffected. + } + } + } - var mostlySorted = ImmutableArray.Create(1, 2, 3, 4, 6, 5, 7, 8, 9, 10); - Assert.Equal(mostlySorted, mostlySorted.Sort(0, 5, Comparer.Default)); - Assert.Equal(mostlySorted, mostlySorted.Sort(5, 5, Comparer.Default)); - Assert.Equal(Enumerable.Range(1, 10), mostlySorted.Sort(4, 2, Comparer.Default)); + public static IEnumerable SortData() + { + return SharedComparers().SelectMany(comparer => + new[] + { + new object[] { new[] { 2, 4, 1, 3 }, 0, 4, comparer, 0 }, + new object[] { new[] { 1 }, 0, 1, comparer, 0 }, + new object[] { new int[0], 0, 0, comparer, 0 }, + new object[] { new[] { 2, 4, 1, 3 }, 1, 2, comparer, 0 }, + new object[] { new[] { 2, 4, 1, 3 }, 4, 0, comparer, 0 }, + new object[] { new[] { "c", "B", "a" }, 0, 3, StringComparer.OrdinalIgnoreCase, string.Empty }, + new object[] { new[] { "c", "B", "a" }, 0, 3, StringComparer.Ordinal, string.Empty }, + new object[] { new[] { 1, 2, 3, 4, 6, 5, 7, 8, 9, 10 }, 4, 2, comparer, 0 } + }); } - [Fact] - public void ToBuilder() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void SortComparisonInvalid(IEnumerable source) { - Assert.Equal(0, s_empty.ToBuilder().Count); - Assert.Throws(() => s_emptyDefault.ToBuilder().Count); + var array = source.ToImmutableArray(); + + Assert.Throws("comparison", () => array.Sort(comparison: null)); + } - var builder = s_oneElement.ToBuilder(); - Assert.Equal(s_oneElement.ToArray(), builder); + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void SortComparerInvalid(IEnumerable source) + { + var array = source.ToImmutableArray(); - builder = s_manyElements.ToBuilder(); - Assert.Equal(s_manyElements.ToArray(), builder); + Assert.Throws("index", () => array.Sort(-1, -1, Comparer.Default)); + Assert.Throws("index", () => array.Sort(-1, 0, Comparer.Default)); - // Make sure that changing the builder doesn't change the original immutable array. - int expected = s_manyElements[0]; - builder[0] = expected + 1; - Assert.Equal(expected, s_manyElements[0]); - Assert.Equal(expected + 1, builder[0]); + Assert.Throws("count", () => array.Sort(0, -1, Comparer.Default)); + Assert.Throws("count", () => array.Sort(array.Length + 1, 0, Comparer.Default)); + Assert.Throws("count", () => array.Sort(0, array.Length + 1, Comparer.Default)); } - [Fact] - public void StructuralEquatableEqualsDefault() + [Theory] + [InlineData(-1, -1)] + [InlineData(0, 0)] + [InlineData(1, 1)] + public void SortDefaultInvalid(int index, int count) { - IStructuralEquatable eq = s_emptyDefault; + Assert.All(SharedComparers(), comparer => + { + Assert.Throws(() => s_emptyDefault.Sort()); + Assert.Throws(() => s_emptyDefault.Sort(comparer)); + Assert.Throws(() => s_emptyDefault.Sort(comparer.Compare)); + Assert.Throws(() => s_emptyDefault.Sort(index, count, comparer)); + }); + } + + [Theory] + [MemberData(nameof(SortAlreadySortedData))] + public void SortAlreadySorted(IEnumerable source, int index, int count, IComparer comparer) + { + // If ImmutableArray.Sort is called when the array is already sorted, + // it should just return the original array rather than allocating a new one. + + var array = source.ToImmutableArray(); + + Assert.True(array == array.Sort(index, count, comparer)); + + if (index == 0 && count == array.Length) + { + Assert.True(array == array.Sort(comparer)); - Assert.True(eq.Equals(s_emptyDefault, EqualityComparer.Default)); - Assert.False(eq.Equals(s_empty, EqualityComparer.Default)); - Assert.False(eq.Equals(s_oneElement, EqualityComparer.Default)); + if (comparer != null) + { + Assert.True(array == array.Sort(comparer.Compare)); + } + + if (comparer == null || comparer == Comparer.Default) + { + Assert.True(array == array.Sort()); + } + } } - [Fact] - public void StructuralEquatableEquals() - { - IStructuralEquatable array = new int[3] { 1, 2, 3 }; - IStructuralEquatable immArray = ImmutableArray.Create(1, 2, 3); - - var otherArray = new object[] { 1, 2, 3 }; - var otherImmArray = ImmutableArray.Create(otherArray); - var unequalArray = new int[] { 1, 2, 4 }; - var unequalImmArray = ImmutableArray.Create(unequalArray); - var unrelatedArray = new string[3]; - var unrelatedImmArray = ImmutableArray.Create(unrelatedArray); - var otherList = new List { 1, 2, 3 }; - Assert.Equal(array.Equals(otherArray, EqualityComparer.Default), immArray.Equals(otherImmArray, EqualityComparer.Default)); - Assert.Equal(array.Equals(otherList, EqualityComparer.Default), immArray.Equals(otherList, EqualityComparer.Default)); - Assert.Equal(array.Equals(unrelatedArray, EverythingEqual.Default), immArray.Equals(unrelatedImmArray, EverythingEqual.Default)); - Assert.Equal(array.Equals(new object(), EqualityComparer.Default), immArray.Equals(new object(), EqualityComparer.Default)); - Assert.Equal(array.Equals(null, EqualityComparer.Default), immArray.Equals(null, EqualityComparer.Default)); - Assert.Equal(array.Equals(unequalArray, EqualityComparer.Default), immArray.Equals(unequalImmArray, EqualityComparer.Default)); + public static IEnumerable SortAlreadySortedData() + { + yield return new object[] { new[] { 1, 2, 3, 4 }, 0, 4, null }; + yield return new object[] { new[] { 1, 2, 3, 4, 6, 5, 7, 8, 9, 10 }, 0, 5, null }; + yield return new object[] { new[] { 1, 2, 3, 4, 6, 5, 7, 8, 9, 10 }, 5, 5, null }; + + yield return new object[] { new[] { 1, 2, 3, 4 }, 0, 4, Comparer.Default }; + yield return new object[] { new[] { 1, 2, 3, 4, 6, 5, 7, 8, 9, 10 }, 0, 5, Comparer.Default }; + yield return new object[] { new[] { 1, 2, 3, 4, 6, 5, 7, 8, 9, 10 }, 5, 5, Comparer.Default }; + + yield return new object[] { new[] { 1, 5, 2 }, 0, 3, Comparer.Create((x, y) => 0) }; + yield return new object[] { new[] { 1, 5, 2 }, 1, 2, Comparer.Create((x, y) => 0) }; + yield return new object[] { new[] { 1, 5, 2 }, 1, 1, Comparer.Create((x, y) => 0) }; + yield return new object[] { new[] { 1, 5, 2 }, 0, 2, Comparer.Create((x, y) => 0) }; + yield return new object[] { new[] { 1, 5, 2, 4 }, 1, 2, Comparer.Create((x, y) => 0) }; } - [Fact] - public void StructuralEquatableEqualsArrayInterop() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void ToBuilder(IEnumerable source) { - IStructuralEquatable array = new int[3] { 1, 2, 3 }; - IStructuralEquatable immArray = ImmutableArray.Create(1, 2, 3); - var unequalArray = new int[] { 1, 2, 4 }; + var array = source.ToImmutableArray(); + ImmutableArray.Builder builder = array.ToBuilder(); + + Assert.Equal(array, builder); + Assert.Equal(array.Length, builder.Count); - Assert.True(immArray.Equals(array, EqualityComparer.Default)); - Assert.False(immArray.Equals(unequalArray, EqualityComparer.Default)); + // Make sure that mutating the builder doesn't change the ImmutableArray. + if (array.Length > 0) + { + builder[0] += 1; + Assert.Equal(source.First(), array[0]); + Assert.Equal(source.First() + 1, builder[0]); + builder[0] -= 1; + } + + builder.Add(int.MinValue); + Assert.Equal(array.Length + 1, builder.Count); + Assert.Equal(array.Add(int.MinValue), builder); } - [Fact] - public void IStructuralEquatable_Equals_NullComparerNonNullUnderlyingArray_ThrowsNullReferenceException() + [Theory] + [MemberData(nameof(IStructuralEquatableEqualsData))] + [MemberData(nameof(IStructuralEquatableEqualsNullComparerData))] + public void IStructuralEquatableEquals(StrongBox> firstBox, StrongBox secondBox, IEqualityComparer comparer, bool expected) { - // This was not fixed for compatability reasons. See #13410 - IStructuralEquatable equatable = ImmutableArray.Create(1, 2, 3); + // Once https://github.com/xunit/assert.xunit/pull/5 comes into corefx, all the StrongBox stuff can be removed. + + ImmutableArray first = firstBox.Value.ToImmutableArray(); + object second = secondBox.Value; + + Assert.Equal(expected, ((IStructuralEquatable)first).Equals(second, comparer)); - Assert.True(equatable.Equals(equatable, null)); - Assert.Throws(() => equatable.Equals(ImmutableArray.Create(1, 2, 3), null)); - Assert.False(equatable.Equals(new ImmutableArray(), null)); - Assert.False(equatable.Equals(null, null)); + if (!first.IsDefault) + { + int[] firstArray = first.ToArray(); + Assert.Equal(!IsImmutableArray(second) && expected, ((IStructuralEquatable)firstArray).Equals(second, comparer)); + + var secondEquatable = second as IStructuralEquatable; + if (secondEquatable != null) + { + Assert.Equal(expected, secondEquatable.Equals(firstArray, comparer)); + } + } } - [Fact] - public void IStructuralEquatable_Equals_NullComparerNullUnderlyingArray_Works() + public static IEnumerable IStructuralEquatableEqualsData() + { + // The comparers here must consider two arrays structurally equal if the default comparer does. + var optimisticComparers = new IEqualityComparer[] + { + EqualityComparer.Default, + new DelegateEqualityComparer(objectEquals: (x, y) => true) + }; + + // The comparers here must not consider two arrays structurally equal if the default comparer doesn't. + var pessimisticComparers = new IEqualityComparer[] + { + EqualityComparer.Default, + new DelegateEqualityComparer(objectEquals: (x, y) => false) + }; + + foreach (IEqualityComparer comparer in optimisticComparers) + { + yield return new object[] { new StrongBox>(s_empty), new StrongBox(s_empty), comparer, true }; + yield return new object[] { new StrongBox>(s_emptyDefault), new StrongBox(s_emptyDefault), comparer, true }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(new[] { 1, 2, 3 }), comparer, true }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(ImmutableArray.Create(1, 2, 3)), comparer, true }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(new object[] { 1, 2, 3 }), comparer, true }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(ImmutableArray.Create(1, 2, 3)), comparer, true }; + } + + foreach (IEqualityComparer comparer in pessimisticComparers) + { + yield return new object[] { new StrongBox>(s_emptyDefault), new StrongBox(s_empty), comparer, false }; + yield return new object[] { new StrongBox>(s_emptyDefault), new StrongBox(s_oneElement), comparer, false }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(new List { 1, 2, 3 }), comparer, false }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(new object()), comparer, false }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(null), comparer, false }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(new[] { 1, 2, 4 }), comparer, false }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(ImmutableArray.Create(1, 2, 4)), comparer, false }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(new string[3]), comparer, false }; + yield return new object[] { new StrongBox>(new[] { 1, 2, 3 }), new StrongBox(ImmutableArray.Create(new string[3])), comparer, false }; + } + } + + public static IEnumerable IStructuralEquatableEqualsNullComparerData() { - IStructuralEquatable equatable = new ImmutableArray(); - Assert.True(equatable.Equals(equatable, null)); - Assert.False(equatable.Equals(ImmutableArray.Create(1, 2, 3), null)); - Assert.True(equatable.Equals(new ImmutableArray(), null)); - Assert.Throws(() => equatable.Equals(null, null)); + // Unlike other methods on ImmutableArray, null comparers are invalid inputs for IStructuralEquatable.Equals. + // However, it will not throw for a null comparer if the array is default and `other` is an ImmutableArray, or + // if Array's IStructuralEquatable.Equals implementation (which it calls under the cover) short-circuits before + // trying to use the comparer. + + yield return new object[] { new StrongBox>(s_emptyDefault), new StrongBox(s_emptyDefault), null, true }; + yield return new object[] { new StrongBox>(s_emptyDefault), new StrongBox(ImmutableArray.Create(1, 2, 3)), null, false }; + + yield return new object[] { new StrongBox>(new int[0]), new StrongBox(null), null, false }; // Array short-circuits because `other` is null + yield return new object[] { new StrongBox>(s_empty), new StrongBox(s_empty), null, true }; // Array short-circuits because the arrays are reference-equal + yield return new object[] { new StrongBox>(new int[0]), new StrongBox(new List()), null, false }; // Array short-circuits because `other` is not an array + yield return new object[] { new StrongBox>(new int[0]), new StrongBox(new int[1]), null, false }; // Array short-circuits because `other.Length` isn't equal + yield return new object[] { new StrongBox>(new int[0]), new StrongBox(new int[0]), null, true }; // For zero-element arrays, Array doesn't have to use the comparer } [Fact] - public void StructuralEquatableGetHashCodeDefault() + public void IStructuralEquatableEqualsNullComparerInvalid() { - IStructuralEquatable defaultImmArray = s_emptyDefault; - Assert.Equal(0, defaultImmArray.GetHashCode(EqualityComparer.Default)); + // This was not fixed for compatability reasons. See https://github.com/dotnet/corefx/issues/13410 + Assert.Throws(() => ((IStructuralEquatable)ImmutableArray.Create(1, 2, 3)).Equals(ImmutableArray.Create(1, 2, 3), comparer: null)); + Assert.Throws(() => ((IStructuralEquatable)s_emptyDefault).Equals(other: null, comparer: null)); } - [Fact] - public void StructuralEquatableGetHashCode() + [Theory] + [MemberData(nameof(IStructuralEquatableGetHashCodeData))] + public void IStructuralEquatableGetHashCode(IEnumerable source, IEqualityComparer comparer) + { + var array = source.ToImmutableArray(); + int expected = ((IStructuralEquatable)source.ToArray()).GetHashCode(comparer); + Assert.Equal(expected, ((IStructuralEquatable)array).GetHashCode(comparer)); + } + + public static IEnumerable IStructuralEquatableGetHashCodeData() { - IStructuralEquatable emptyArray = new int[0]; - IStructuralEquatable emptyImmArray = s_empty; - IStructuralEquatable array = new int[3] { 1, 2, 3 }; - IStructuralEquatable immArray = ImmutableArray.Create(1, 2, 3); + var enumerables = Int32EnumerableData() + .Select(array => array[0]) + .Cast>(); - Assert.Equal(emptyArray.GetHashCode(EqualityComparer.Default), emptyImmArray.GetHashCode(EqualityComparer.Default)); - Assert.Equal(array.GetHashCode(EqualityComparer.Default), immArray.GetHashCode(EqualityComparer.Default)); - Assert.Equal(array.GetHashCode(EverythingEqual.Default), immArray.GetHashCode(EverythingEqual.Default)); + return SharedComparers() + .OfType() + .Except(new IEqualityComparer[] { null }) + .SelectMany(comparer => enumerables.Select(enumerable => new object[] { enumerable, comparer })); } [Fact] - public void IStructuralEquatable_GetHashCode_NullComparerNonNullUnderlyingArray_ThrowsArgumentNullException() + public void IStructuralEquatableGetHashCodeDefault() { - IStructuralEquatable equatable = ImmutableArray.Create(1, 2, 3); - Assert.Throws(() => equatable.GetHashCode(null)); + Assert.All(SharedComparers().OfType(), comparer => + { + // A default ImmutableArray should always hash to the same value, regardless of comparer. + // This includes null, which is included in the set of shared comparers. + Assert.Equal(0, ((IStructuralEquatable)s_emptyDefault).GetHashCode(comparer)); + }); } - [Fact] - public void IStructuralEquatable_GetHashCode_NullComparerNullUnderlyingArray_Works() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void IStructuralEquatableGetHashCodeNullComparerNonNullUnderlyingArrayInvalid(IEnumerable source) { - IStructuralEquatable equatable = new ImmutableArray(); - Assert.Equal(0, equatable.GetHashCode(null)); + var array = source.ToImmutableArray(); + Assert.Throws("comparer", () => ((IStructuralEquatable)array).GetHashCode(comparer: null)); } [Fact] - public void StructuralComparableDefault() + public void IStructuralComparableCompareToDefaultAndDefault() + { + Assert.All(SharedComparers().OfType(), comparer => + { + // Default ImmutableArrays are always considered the same as other default ImmutableArrays, no matter + // what the comparer is. (Even if the comparer is null.) + Assert.Equal(0, ((IStructuralComparable)s_emptyDefault).CompareTo(s_emptyDefault, comparer)); + }); + } + + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void IStructuralComparableCompareToDefaultAndNonDefaultInvalid(IEnumerable source) { - IStructuralComparable def = s_emptyDefault; - IStructuralComparable mt = s_empty; + object other = source.ToImmutableArray(); + var comparers = SharedComparers().OfType().Except(new IComparer[] { null }); - // default to default is fine, and should be seen as equal. - Assert.Equal(0, def.CompareTo(s_emptyDefault, Comparer.Default)); + Assert.All(comparers, comparer => + { + // CompareTo should throw if the arrays are of different lengths. The default ImmutableArray is considered to have + // a different length from every other array, including empty ones. + Assert.Throws("other", () => ((IStructuralComparable)s_emptyDefault).CompareTo(other, comparer)); + Assert.Throws("other", () => ((IStructuralComparable)other).CompareTo(s_emptyDefault, comparer)); + }); + } - // default to empty and vice versa should throw, on the basis that - // arrays compared that are of different lengths throw. Empty vs. default aren't really compatible. - Assert.Throws("other", () => def.CompareTo(s_empty, Comparer.Default)); - Assert.Throws("other", () => mt.CompareTo(s_emptyDefault, Comparer.Default)); + [Theory] + [MemberData(nameof(IStructuralComparableCompareToNullComparerArgumentInvalidData))] + public void IStructuralComparableCompareToNullComparerArgumentInvalid(IEnumerable source, object other) + { + // Because of https://github.com/xunit/assert.xunit/pull/5 we cannot pass in a default ImmutableArray directly, + // so a null source is used as a sentinel value to represent that here. Once that change makes it into corefx, + // just pass in a default array from the MemberData. + + var array = source?.ToImmutableArray() ?? s_emptyDefault; + Assert.Throws("other", () => ((IStructuralComparable)array).CompareTo(other, comparer: null)); + + if (other is Array || IsImmutableArray(other)) + { + Assert.Throws("other", () => ((IStructuralComparable)other).CompareTo(array, comparer: null)); + } } - [Fact] - public void StructuralComparable() + public static IEnumerable IStructuralComparableCompareToNullComparerArgumentInvalidData() { - IStructuralComparable array = new int[3] { 1, 2, 3 }; - IStructuralComparable equalArray = new int[3] { 1, 2, 3 }; - IStructuralComparable immArray = ImmutableArray.Create((int[])array); - IStructuralComparable equalImmArray = ImmutableArray.Create((int[])equalArray); + yield return new object[] { null, null }; + yield return new object[] { null, ImmutableArray.Create(1, 2, 3) }; + yield return new object[] { new[] { 1, 2, 3 }, null }; + } - IStructuralComparable longerArray = new int[] { 1, 2, 3, 4 }; - IStructuralComparable longerImmArray = ImmutableArray.Create((int[])longerArray); + [Theory] + [MemberData(nameof(IStructuralComparableCompareToNullComparerNullReferenceInvalidData))] + public void IStructuralComparableCompareToNullComparerNullReferenceInvalid(IEnumerable source, object other) + { + var array = source.ToImmutableArray(); + Assert.Throws(() => ((IStructuralComparable)array).CompareTo(other, comparer: null)); - Assert.Equal(array.CompareTo(equalArray, Comparer.Default), immArray.CompareTo(equalImmArray, Comparer.Default)); + if (other == null) + { + Assert.Throws(() => ((IStructuralComparable)array).CompareTo(s_emptyDefault, comparer: null)); + } + } - Assert.Throws("other", () => array.CompareTo(longerArray, Comparer.Default)); - Assert.Throws("other", () => immArray.CompareTo(longerImmArray, Comparer.Default)); + public static IEnumerable IStructuralComparableCompareToNullComparerNullReferenceInvalidData() + { + // This was not fixed for compatability reasons. See https://github.com/dotnet/corefx/issues/13410 + yield return new object[] { new[] { 1, 2, 3 }, new[] { 1, 2, 3 } }; + yield return new object[] { new[] { 1, 2, 3 }, ImmutableArray.Create(1, 2, 3) }; + // Cache this into a local so the comparands are reference-equal. + var oneTwoThree = ImmutableArray.Create(1, 2, 3); + yield return new object[] { oneTwoThree, oneTwoThree }; + } - var list = new List { 1, 2, 3 }; - Assert.Throws("other", () => array.CompareTo(list, Comparer.Default)); - Assert.Throws("other", () => immArray.CompareTo(list, Comparer.Default)); + [Theory] + [MemberData(nameof(IStructuralComparableCompareToData))] + public void IStructuralComparableCompareTo(IEnumerable source, object other, IComparer comparer, int expected) + { + var array = source?.ToImmutableArray() ?? s_emptyDefault; + Assert.Equal(expected, ((IStructuralComparable)array).CompareTo(other ?? s_emptyDefault, comparer)); + + if (other is Array) + { + Assert.Equal(expected, ((IStructuralComparable)source.ToArray()).CompareTo(other ?? s_emptyDefault, comparer)); + } } - [Fact] - public void StructuralComparableArrayInterop() + public static IEnumerable IStructuralComparableCompareToData() { - IStructuralComparable array = new int[3] { 1, 2, 3 }; - IStructuralComparable equalArray = new int[3] { 1, 2, 3 }; - IStructuralComparable immArray = ImmutableArray.Create((int[])array); - IStructuralComparable equalImmArray = ImmutableArray.Create((int[])equalArray); + yield return new object[] { new[] { 1, 2, 3 }, new[] { 1, 2, 3 }, Comparer.Default, 0 }; + yield return new object[] { new[] { 1, 2, 3 }, new[] { 1, 2, 3 }, Comparer.Default, 0 }; + + yield return new object[] { new[] { 1, 2, 3 }, ImmutableArray.Create(1, 2, 3), Comparer.Default, 0 }; + yield return new object[] { new[] { 1, 2, 3 }, ImmutableArray.Create(1, 2, 3), Comparer.Default, 0 }; - Assert.Equal(array.CompareTo(equalArray, Comparer.Default), immArray.CompareTo(equalArray, Comparer.Default)); + // The comparands are the same instance, so Array can short-circuit. + yield return new object[] { s_empty, s_empty, Comparer.Default, 0 }; + yield return new object[] { s_empty, s_empty, Comparer.Default, 0 }; + + // Normally, a null comparer is an invalid input. However, if both comparands are default ImmutableArrays + // then CompareTo will short-circuit before it validates the comparer. + yield return new object[] { null, null, null, 0 }; } - [Fact] - public void IStructuralComparable_NullComparerNonNullUnderlyingArray_ThrowsNullReferenceException() + [Theory] + [MemberData(nameof(IStructuralComparableCompareToInvalidData))] + public void IStructuralComparableCompareToInvalid(IEnumerable source, object other, IComparer comparer) { - // This was not fixed for compatability reasons. See #13410 - IStructuralComparable comparable = ImmutableArray.Create(1, 2, 3); - Assert.Throws(() => comparable.CompareTo(comparable, null)); - Assert.Throws(() => comparable.CompareTo(ImmutableArray.Create(1, 2, 3), null)); - Assert.Throws("other", () => comparable.CompareTo(new ImmutableArray(), null)); + var array = source.ToImmutableArray(); + + Assert.Throws("other", () => ((IStructuralComparable)array).CompareTo(other, comparer)); + Assert.Throws("other", () => ((IStructuralComparable)source.ToArray()).CompareTo(other, comparer)); + + if (other is Array || IsImmutableArray(other)) + { + Assert.Throws("other", () => ((IStructuralComparable)other).CompareTo(array, comparer)); + Assert.Throws("other", () => ((IStructuralComparable)other).CompareTo(source.ToArray(), comparer)); + } } - [Fact] - public void IStructuralComparable_NullComparerNullUnderlyingArray_Works() + public static IEnumerable IStructuralComparableCompareToInvalidData() { - IStructuralComparable comparable = new ImmutableArray(); - Assert.Equal(0, comparable.CompareTo(comparable, null)); - Assert.Throws("other", () => comparable.CompareTo(null, null)); - Assert.Throws("other", () => comparable.CompareTo(ImmutableArray.Create(1, 2, 3), null)); - Assert.Equal(0, comparable.CompareTo(new ImmutableArray(), null)); + return SharedComparers() + .OfType() + .Except(new IComparer[] { null }) + .SelectMany(comparer => new[] + { + new object[] { new[] { 1, 2, 3 }, new[] { 1, 2, 3, 4 }, comparer }, + new object[] { new[] { 1, 2, 3 }, ImmutableArray.Create(1, 2, 3, 4), comparer }, + new object[] { new[] { 1, 2, 3 }, new List { 1, 2, 3 }, comparer } + }); } [Theory] - [InlineData(new int[0], 5)] - [InlineData(new int[] { 3 }, 5)] - [InlineData(new int[] { 5 }, 5)] - [InlineData(new int[] { 1, 2, 3 }, 1)] - [InlineData(new int[] { 1, 2, 3 }, 2)] - [InlineData(new int[] { 1, 2, 3 }, 3)] - [InlineData(new int[] { 1, 2, 3, 4 }, 4)] - public void BinarySearch(int[] array, int value) + [MemberData(nameof(BinarySearchData))] + public void BinarySearch(IEnumerable source, int value) { - Assert.Throws("array", () => ImmutableArray.BinarySearch(default(ImmutableArray), value)); + var array = source.ToArray(); Assert.Equal( Array.BinarySearch(array, value), @@ -1446,17 +2119,35 @@ public void BinarySearch(int[] array, int value) ImmutableArray.BinarySearch(ImmutableArray.Create(array), 0, array.Length, value, Comparer.Default)); } + public static IEnumerable BinarySearchData() + { + yield return new object[] { new int[0], 5 }; + yield return new object[] { new[] { 3 }, 5 }; + yield return new object[] { new[] { 5 }, 5 }; + yield return new object[] { new[] { 1, 2, 3 }, 1 }; + yield return new object[] { new[] { 1, 2, 3 }, 2 }; + yield return new object[] { new[] { 1, 2, 3 }, 3 }; + yield return new object[] { new[] { 1, 2, 3, 4 }, 4 }; + } + + [Theory] + [MemberData(nameof(BinarySearchData))] + public void BinarySearchDefaultInvalid(IEnumerable source, int value) + { + Assert.Throws("array", () => ImmutableArray.BinarySearch(s_emptyDefault, value)); + } + [Fact] public void OfType() { - Assert.Equal(0, s_emptyDefault.OfType().Count()); - Assert.Equal(0, s_empty.OfType().Count()); - Assert.Equal(1, s_oneElement.OfType().Count()); - Assert.Equal(1, s_twoElementRefTypeWithNull.OfType().Count()); + Assert.Equal(new int[0], s_emptyDefault.OfType()); + Assert.Equal(new int[0], s_empty.OfType()); + Assert.Equal(s_oneElement, s_oneElement.OfType()); + Assert.Equal(new[] { "1" }, s_twoElementRefTypeWithNull.OfType()); } [Fact] - public void Add_ThreadSafety() + public void AddThreadSafety() { // Note the point of this thread-safety test is *not* to test the thread-safety of the test itself. // This test has a known issue where the two threads will stomp on each others updates, but that's not the point. @@ -1477,11 +2168,11 @@ public void Add_ThreadSafety() Task.WaitAll(Task.Run(mutator), Task.Run(mutator)); } - [Fact] - public void DebuggerAttributesValid() + [Theory] + [MemberData(nameof(Int32EnumerableData))] + public void DebuggerAttributesValid(IEnumerable source) { - DebuggerAttributes.ValidateDebuggerDisplayReferences(ImmutableArray.Create()); // verify empty - DebuggerAttributes.ValidateDebuggerDisplayReferences(ImmutableArray.Create(1, 2, 3)); // verify non-empty + DebuggerAttributes.ValidateDebuggerDisplayReferences(source.ToImmutableArray()); } protected override IEnumerable GetEnumerableOf(params T[] contents) @@ -1489,6 +2180,111 @@ protected override IEnumerable GetEnumerableOf(params T[] contents) return ImmutableArray.Create(contents); } + /// + /// Returns an object typed as an . + /// + /// The type of the object. + /// The object. + private static IEquatable AsEquatable(T obj) where T : IEquatable => obj; + + /// + /// Given an enumerable, produces a list of enumerables that have the same contents, + /// but have different underlying types. + /// + /// The element type. + /// The source enumerable. + private static IEnumerable> ChangeType(IEnumerable source) + { + yield return source; + // Implements IList, but isn't a type we're likely to explicitly optimize for. + yield return new LinkedList(source); + yield return source.Select(x => x); // A lazy enumerable. + + // Constructing these types will be problematic if the source is a T[], but + // its underlying type is not typeof(T[]). + // The reason is since they are contiguous in memory, they call Array.Copy + // if the source is an array as an optimization, which throws if the types + // of the arrays do not exactly match up. + // More info here: https://github.com/dotnet/corefx/issues/2241 + + if (!(source is T[]) || source.GetType() == typeof(T[])) + { + yield return source.ToArray(); + yield return source.ToList(); + yield return source.ToImmutableArray(); + // Is not an ICollection, but does implement ICollection and IReadOnlyCollection. + yield return new Queue(source); + } + } + + /// + /// Wraps an enumerable in an iterator, so that it does not implement interfaces such as . + /// + /// The element type. + /// The source enumerable. + private static IEnumerable ForceLazy(IEnumerable source) + { + foreach (T element in source) + { + yield return element; + } + } + + /// + /// Gets the underlying array of an . For testing purposes only. + /// + /// The element type. + /// The immutable array. + /// The underlying array. + private static T[] GetUnderlyingArray(ImmutableArray array) + { + FieldInfo arrayField = typeof(ImmutableArray) + .GetField("array", BindingFlags.Instance | BindingFlags.NonPublic); + return (T[])arrayField.GetValue(array); + } + + /// + /// Returns whether the object is an instance of an . + /// + /// The object. + private static bool IsImmutableArray(object obj) + { + if (obj == null) + { + return false; + } + + var typeInfo = obj.GetType().GetTypeInfo(); + return typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(ImmutableArray<>); + } + + /// + /// Returns a list of comparers that are shared between tests. + /// + /// The comparand type. + private static IEnumerable> SharedComparers() + where T : IComparable + { + // Null comparers should be accepted and translated to the default comparer. + yield return null; + yield return Comparer.Default; + yield return Comparer.Create((x, y) => y.CompareTo(x)); + yield return Comparer.Create((x, y) => 0); + } + + /// + /// Returns a list of equality comparers that are shared between tests. + /// + /// The comparand type. + private static IEnumerable> SharedEqualityComparers() + { + // Null comparers should be accepted and translated to the default comparer. + yield return null; + yield return EqualityComparer.Default; + yield return new DelegateEqualityComparer(equals: (x, y) => true, objectGetHashCode: obj => 0); + yield return new DelegateEqualityComparer(equals: (x, y) => false, objectGetHashCode: obj => 0); + } + /// /// A structure that takes exactly 3 bytes of memory. /// diff --git a/src/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj b/src/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj index a31faf62afec..66537859fd4f 100644 --- a/src/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj +++ b/src/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj @@ -57,6 +57,9 @@ Common\System\Collections\CollectionAsserts.cs + + Common\System\Collections\DelegateEqualityComparer.cs + Common\System\Collections\ICollection.NonGeneric.Tests.cs