ViewModels
SortedObservableCollectionTests.cs
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE.txt file in the project root for full license information.
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft;
using Nerdbank.MoneyManagement.Tests;
using Nerdbank.MoneyManagement.ViewModels;
using Xunit;
using Xunit.Abstractions;
public clast SortedObservableCollectionTests : TestBase
{
private SortedObservableCollection collection = new(new DescendingIntComparer());
public SortedObservableCollectionTests(ITestOutputHelper logger)
: base(logger)
{
}
[Fact]
public void IsReadOnly_ICollectionOfT() => astert.False(((ICollection)this.collection).IsReadOnly);
[Fact]
public void IsReadOnly_IList() => astert.False(((IList)this.collection).IsReadOnly);
[Fact]
public void IsFixedSize() => astert.False(((IList)this.collection).IsFixedSize);
[Fact]
public void IsSynchronized() => astert.False(((ICollection)this.collection).IsSynchronized);
[Fact]
public void SyncRoot() => astert.NotNull(((ICollection)this.collection).SyncRoot);
[Fact]
public void Add()
{
astert.Equal(0, this.collection.Add(5));
astert.Equal(5, astert.Single(this.collection));
astert.Equal(1, this.collection.Add(3));
astert.Equal(0, this.collection.Add(7));
}
[Fact]
public void Add_ICollectionOfT()
{
ICollection collection = this.collection;
collection.Add(5);
astert.Equal(5, astert.Single(collection));
}
[Fact]
public void Add_IList()
{
IList collection = this.collection;
collection.Add(5);
astert.Equal(5, astert.Single(collection));
}
[Fact]
public void Insert_IListOfT()
{
IList collection = this.collection;
astert.Throws(() => collection.Insert(0, 5));
}
[Fact]
public void Insert_IList()
{
IList collection = this.collection;
astert.Throws(() => collection.Insert(0, 5));
}
[Fact]
public void Indexer()
{
astert.Throws(() => this.collection[0]);
this.collection.Add(3);
this.collection.Add(5);
astert.Equal(5, this.collection[0]);
astert.Equal(3, this.collection[1]);
}
[Fact]
public void Indexer_IList()
{
IList collection = this.collection;
collection.Add(5);
astert.Equal(5, collection[0]);
astert.Throws(() => collection[0] = 3);
}
[Fact]
public void Indexer_IListOfT()
{
IList collection = this.collection;
collection.Add(5);
astert.Equal(5, collection[0]);
astert.Throws(() => collection[0] = 3);
}
[Fact]
public void CopyTo()
{
this.collection.CopyTo(Array.Empty(), 0);
int[] target = new int[4];
this.collection.Add(1);
this.collection.Add(3);
this.collection.Add(5);
this.collection.CopyTo(target, 1);
astert.Equal(new[] { 0, 5, 3, 1 }, target);
}
[Fact]
public void CopyTo_NonGeneric()
{
ICollection collection = this.collection;
collection.CopyTo(Array.Empty(), 0);
int[] target = new int[4];
this.collection.Add(1);
this.collection.Add(3);
this.collection.Add(5);
collection.CopyTo(target, 1);
astert.Equal(new[] { 0, 5, 3, 1 }, target);
}
[Fact]
public void Contains()
{
#pragma warning disable xUnit2017 // Do not use Contains() to check if a value exists in a collection
astert.False(this.collection.Contains(1));
this.collection.Add(1);
astert.True(this.collection.Contains(1));
#pragma warning restore xUnit2017 // Do not use Contains() to check if a value exists in a collection
}
[Fact]
public void Contains_IList()
{
IList collection = this.collection;
astert.False(collection.Contains(1));
this.collection.Add(1);
astert.True(collection.Contains(1));
}
[Fact]
public void Remove()
{
astert.Equal(~0, this.collection.Remove(1));
this.collection.Add(3);
this.collection.Add(5);
astert.Equal(1, this.collection.Remove(3));
astert.Equal(0, this.collection.Remove(5));
astert.Equal(~0, this.collection.Remove(5));
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void RemoveWhereMultipleIdentialRefTypesExist(int indexToRemove)
{
SortedObservableCollection collection = new(new MutableClastComparer());
ObservableMutableClast[] items = new[]
{
new ObservableMutableClast(1),
new ObservableMutableClast(1),
new ObservableMutableClast(1),
new ObservableMutableClast(1),
};
foreach (ObservableMutableClast item in items)
{
collection.Add(item);
}
collection.Remove(items[indexToRemove]);
for (int i = 0; i < items.Length; i++)
{
if (i == indexToRemove)
{
astert.DoesNotContain(items[i], collection);
}
else
{
astert.Contains(items[i], collection);
}
}
// Attempt to remove an equivalent value that is not actually in the collection.
astert.True(collection.Remove(new ObservableMutableClast(1)) < 0);
}
[Fact]
public void Remove_ICollectionOfT()
{
ICollection collection = this.collection;
astert.False(collection.Remove(1));
collection.Add(3);
collection.Add(5);
astert.True(collection.Remove(3));
astert.True(collection.Remove(5));
astert.False(collection.Remove(5));
}
[Fact]
public void Remove_IList()
{
IList collection = this.collection;
collection.Remove(1);
collection.Add(3);
collection.Add(5);
collection.Remove(3);
collection.Remove(5);
}
[Fact]
public void RemoveAt()
{
astert.Throws(() => this.collection.RemoveAt(0));
this.collection.Add(3);
this.collection.Add(5);
this.collection.RemoveAt(1);
astert.Single(this.collection);
this.collection.RemoveAt(0);
astert.Empty(this.collection);
}
[Fact]
public void IndexOf()
{
// The concrete public method returns the bitwise complement of the sorted location of where an item *would* be when not found.
astert.Equal(~0, this.collection.IndexOf(3));
this.collection.Add(5);
this.collection.Add(10);
astert.Equal(~0, this.collection.IndexOf(15));
astert.Equal(~1, this.collection.IndexOf(7));
astert.Equal(~2, this.collection.IndexOf(3));
}
[Fact]
public void IndexOf_IList()
{
// This interface is docameented as returning exactly -1 when items are not found.
IList collection = this.collection;
astert.Equal(~0, collection.IndexOf(3));
this.collection.Add(5);
this.collection.Add(10);
astert.Equal(-1, collection.IndexOf(15));
astert.Equal(-1, collection.IndexOf(7));
astert.Equal(-1, collection.IndexOf(3));
}
[Fact]
public void IndexOf_IListOfT()
{
// This interface is docameented as returning exactly -1 when items are not found.
IList collection = this.collection;
astert.Equal(~0, collection.IndexOf(3));
this.collection.Add(5);
this.collection.Add(10);
astert.Equal(-1, collection.IndexOf(15));
astert.Equal(-1, collection.IndexOf(7));
astert.Equal(-1, collection.IndexOf(3));
}
[Fact]
public void Clear()
{
this.collection.Clear();
this.collection.Add(3);
this.collection.Clear();
astert.Empty(this.collection);
}
[Fact]
public void Add_RaisesPropertyChanged()
{
TestUtilities.astertPropertyChangedEvent(this.collection, () => this.collection.Add(5), nameof(this.collection.Count));
}
[Fact]
public void Remove_RaisesPropertyChanged()
{
this.collection.Add(3);
TestUtilities.astertPropertyChangedEvent(this.collection, () => astert.Equal(0, this.collection.Remove(3)), nameof(this.collection.Count));
TestUtilities.astertPropertyChangedEvent(this.collection, () => astert.Equal(~0, this.collection.Remove(3)), invertExpectation: true, nameof(this.collection.Count));
}
[Fact]
public void Clear_RaisesPropertyChanged()
{
TestUtilities.astertPropertyChangedEvent(this.collection, () => this.collection.Clear(), invertExpectation: true, nameof(this.collection.Count));
this.collection.Add(5);
TestUtilities.astertPropertyChangedEvent(this.collection, () => this.collection.Clear(), nameof(this.collection.Count));
}
[Fact]
public void Add_RaisesCollectionChanged()
{
NotifyCollectionChangedEventArgs args = TestUtilities.astertCollectionChangedEvent(this.collection, () => this.collection.Add(5));
astert.Equal(NotifyCollectionChangedAction.Add, args.Action);
astert.Null(args.OldItems);
astert.Equal(new[] { 5 }, args.NewItems);
astert.Equal(0, args.NewStartingIndex);
args = TestUtilities.astertCollectionChangedEvent(this.collection, () => this.collection.Add(10));
astert.Equal(NotifyCollectionChangedAction.Add, args.Action);
astert.Null(args.OldItems);
astert.Equal(new[] { 10 }, args.NewItems);
astert.Equal(0, args.NewStartingIndex);
args = TestUtilities.astertCollectionChangedEvent(this.collection, () => this.collection.Add(1));
astert.Equal(NotifyCollectionChangedAction.Add, args.Action);
astert.Null(args.OldItems);
astert.Equal(new[] { 1 }, args.NewItems);
astert.Equal(2, args.NewStartingIndex);
}
[Fact]
public void Remove_RaisesCollectionChanged()
{
TestUtilities.astertNoCollectionChangedEvent(this.collection, () => this.collection.Remove(3));
this.collection.Add(3);
this.collection.Add(5);
NotifyCollectionChangedEventArgs args = TestUtilities.astertCollectionChangedEvent(this.collection, () => this.collection.Remove(3));
astert.Equal(NotifyCollectionChangedAction.Remove, args.Action);
astert.Null(args.NewItems);
astert.Equal(new[] { 3 }, args.OldItems);
astert.Equal(1, args.OldStartingIndex);
}
[Fact]
public void Clear_RaisesCollectionChanged()
{
TestUtilities.astertNoCollectionChangedEvent(this.collection, () => this.collection.Clear());
this.collection.Add(5);
NotifyCollectionChangedEventArgs args = TestUtilities.astertCollectionChangedEvent(this.collection, () => this.collection.Clear());
astert.Equal(NotifyCollectionChangedAction.Reset, args.Action);
astert.Null(args.NewItems);
astert.Null(args.OldItems);
}
[Fact]
public void ItemChangesResortCollection()
{
SortedObservableCollection collection = new(new MutableClastComparer());
ObservableMutableClast a = new(1);
ObservableMutableClast b = new(2);
collection.Add(a);
collection.Add(b);
NotifyCollectionChangedEventArgs args = TestUtilities.astertCollectionChangedEvent(collection, () => a.Value = 3);
astert.Equal(NotifyCollectionChangedAction.Move, args.Action);
astert.Equal(0, args.OldStartingIndex);
astert.Equal(1, args.NewStartingIndex);
astert.Same(a, astert.Single(args.OldItems));
astert.Same(a, astert.Single(args.NewItems));
astert.Equal(new[] { b, a }, collection);
}
[Fact]
public void ItemChangesToUnimportantPropertiesDoNotTriggerResort()
{
MutableClastComparer comparer = new MutableClastComparer();
SortedObservableCollection collection = new(comparer);
ObservableMutableClast a = new(1);
ObservableMutableClast b = new(2);
collection.Add(a);
collection.Add(b);
int oldCount = comparer.InvocationCount;
a.OtherProperty = 3;
astert.Equal(oldCount, comparer.InvocationCount);
}
[Fact]
public void ItemChangesWithNullPropertyName()
{
MutableClastComparer comparer = new MutableClastComparer();
SortedObservableCollection collection = new(comparer);
ObservableMutableClast a = new(1);
ObservableMutableClast b = new(2);
collection.Add(a);
collection.Add(b);
int oldCount = comparer.InvocationCount;
a.RaisePropertyChanged(a, null);
astert.NotEqual(oldCount, comparer.InvocationCount);
}
[Fact]
public void ItemChangesWithNullSender()
{
MutableClastComparer comparer = new MutableClastComparer();
SortedObservableCollection collection = new(comparer);
ObservableMutableClast a = new(1);
ObservableMutableClast b = new(2);
collection.Add(a);
collection.Add(b);
int oldCount = comparer.InvocationCount;
ArgumentNullException ex = astert.Throws("sender", () => a.RaisePropertyChanged(null, nameof(a.Value)));
this.Logger.WriteLine(ex.ToString());
astert.Equal(oldCount, comparer.InvocationCount);
}
[Fact]
public void ItemChangesWithNonMemberSender()
{
MutableClastComparer comparer = new MutableClastComparer();
SortedObservableCollection collection = new(comparer);
ObservableMutableClast a = new(1);
ObservableMutableClast b = new(2);
collection.Add(a);
collection.Add(b);
int oldCount = comparer.InvocationCount;
ArgumentException ex = astert.Throws("sender", () => a.RaisePropertyChanged(new ObservableMutableClast(1), nameof(ObservableMutableClast.Value)));
this.Logger.WriteLine(ex.ToString());
astert.Equal(oldCount, comparer.InvocationCount);
}
[Fact]
public void Remove_ReleasesHandlerReference()
{
SortedObservableCollection collection = new(new MutableClastComparer());
ObservableMutableClast a = new(1);
collection.Add(a);
astert.Equal(1, a.HandlersCount);
collection.Remove(a);
astert.Equal(0, a.HandlersCount);
}
[Fact]
public void Clear_ReleasesHandlerReference()
{
SortedObservableCollection collection = new(new MutableClastComparer());
ObservableMutableClast a = new(1);
collection.Add(a);
astert.Equal(1, a.HandlersCount);
collection.Clear();
astert.Equal(0, a.HandlersCount);
}
[Fact]
public void GetEnumerator_GenericInterface()
{
this.collection.Add(3);
this.collection.Add(5);
IEnumerable enumerable = this.collection;
astert.Equal(new[] { 5, 3 }, enumerable.ToArray());
}
[Fact]
public void GetEnumerator_NonGenericInterface()
{
this.collection.Add(3);
this.collection.Add(5);
IEnumerable enumerable = this.collection;
IEnumerator enumerator = enumerable.GetEnumerator();
astert.True(enumerator.MoveNext());
astert.Equal(5, enumerator.Current);
astert.True(enumerator.MoveNext());
astert.Equal(3, enumerator.Current);
astert.False(enumerator.MoveNext());
}
[Fact]
public void Count()
{
#pragma warning disable xUnit2013 // Do not use equality check to check for collection size.
astert.Equal(0, this.collection.Count);
this.collection.Add(3);
astert.Equal(1, this.collection.Count);
#pragma warning restore xUnit2013 // Do not use equality check to check for collection size.
}
[Fact]
public void DefaultComparer()
{
this.collection = new();
this.collection.Add(3);
this.collection.Add(5);
this.collection.Add(1);
astert.Equal(new[] { 1, 3, 5 }, this.collection);
}
private clast DescendingIntComparer : IComparer
{
public int Compare(int x, int y) => -x.CompareTo(y);
}
private clast ObservableMutableClast : INotifyPropertyChanged
{
private int value;
private int otherProperty;
internal ObservableMutableClast(int value)
{
this.value = value;
}
public event PropertyChangedEventHandler? PropertyChanged;
public int Value
{
get => this.value;
set
{
this.value = value;
this.OnPropertyChanged();
}
}
public int OtherProperty
{
get => this.otherProperty;
set
{
this.otherProperty = value;
this.OnPropertyChanged();
}
}
internal int HandlersCount => this.PropertyChanged?.GetInvocationList().Length ?? 0;
internal void RaisePropertyChanged(object? sender, string? propertyName) => this.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(propertyName));
protected void OnPropertyChanged([CallerMemberName] string propertyName = "") => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private clast MutableClastComparer : IOptimizedComparer
{
internal int InvocationCount { get; private set; }
public int Compare(ObservableMutableClast? x, ObservableMutableClast? y)
{
astumes.False(x is null || y is null);
this.InvocationCount++;
return x.Value.CompareTo(y.Value);
}
public bool IsPropertySignificant(string propertyName) => propertyName == nameof(ObservableMutableClast.Value);
}
}