Skip to content

Commit d26b63c

Browse files
jcummings2John Cummings
andauthored
Fix Exception in list static filter index assumptions (#1120)
* Add RemoveKey tests showing issue with Refresh and Filter * Fix index out of range issue with static List Filter * Remove unused variable in RemoveKeyFixture * Remove unused variable in RemoveKeyFixture (really) * Revert formatting to previous in Filter.Static.cs * Add unit test changing order or RemoveKey call * Update RemoveKey test names based on PR feedback * Remove extraneous comment per PR feedback * Move Filter-related RemoveKey tests to FilterFixture --------- Co-authored-by: John Cummings <jcummings2sf@gmail.com>
1 parent f933ae5 commit d26b63c

3 files changed

Lines changed: 161 additions & 4 deletions

File tree

src/DynamicData.Tests/Cache/FilterFixture.Static.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
using Xunit;
77

88
using DynamicData.Tests.Utilities;
9+
using DynamicData.Tests.Domain;
10+
using System.Collections.ObjectModel;
11+
using System.Linq;
912

1013
namespace DynamicData.Tests.Cache;
1114

@@ -86,5 +89,55 @@ protected override IObservable<IChangeSet<Item, int>> BuildUut(
8689
=> source.Filter(
8790
filter: predicate,
8891
suppressEmptyChangeSets: suppressEmptyChangeSets);
92+
[Fact]
93+
public void AutoRefreshRemoveKeyFilterUpdate_CollectionUpdated()
94+
{
95+
RandomPersonGenerator generator = new();
96+
using var source = new SourceCache<Person, string>(p => p.Key);
97+
var people = generator.Take(100).ToArray();
98+
var average = people.Average(x => x.Age);
99+
ReadOnlyObservableCollection<Person> collection;
100+
using var subscription = source.Connect()
101+
.AutoRefresh(x => x.Age)
102+
.RemoveKey()
103+
.Filter(x => x.Age < average)
104+
.Bind(out collection)
105+
.Subscribe();
106+
source.AddOrUpdate(people);
107+
108+
Assert.Equivalent(people.Where(x => x.Age < average), collection);
109+
110+
foreach (var person in people)
111+
{
112+
person.Age = person.Age + 1;
113+
}
114+
Assert.Equivalent(people.Where(x => x.Age < average), collection);
115+
}
116+
117+
[Fact]
118+
public void AutoRefreshFilterRemoveKeyUpdate_CollectionUpdated()
119+
{
120+
RandomPersonGenerator generator = new();
121+
using var source = new SourceCache<Person, string>(p => p.Key);
122+
var people = generator.Take(100).ToArray();
123+
var average = people.Average(x => x.Age);
124+
ReadOnlyObservableCollection<Person> collection;
125+
using var subscription = source.Connect()
126+
.AutoRefresh(x => x.Age)
127+
.Filter(x => x.Age < average)
128+
.RemoveKey()
129+
.Bind(out collection)
130+
.Subscribe();
131+
source.AddOrUpdate(people);
132+
133+
Assert.Equivalent(people.Where(x => x.Age < average), collection);
134+
135+
foreach (var person in people)
136+
{
137+
person.Age = person.Age + 1;
138+
}
139+
Assert.Equivalent(people.Where(x => x.Age < average), collection);
140+
}
89141
}
142+
90143
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#region
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Collections.ObjectModel;
6+
using System.Linq;
7+
using System.Reactive.Disposables;
8+
9+
using DynamicData.Binding;
10+
using DynamicData.Tests.Domain;
11+
12+
using FluentAssertions;
13+
14+
using Xunit;
15+
16+
#endregion
17+
18+
namespace DynamicData.Tests.Cache;
19+
20+
public class RemoveKeyFixture : IDisposable
21+
{
22+
private readonly RandomPersonGenerator _generator = new();
23+
24+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Handled with CompositeDisposable")]
25+
private readonly ISourceCache<Person, string> _source;
26+
27+
private readonly CompositeDisposable _cleanup = new();
28+
29+
public RemoveKeyFixture()
30+
{
31+
_source = new SourceCache<Person, string>(p => p.Key);
32+
_cleanup.Add(_source);
33+
}
34+
35+
public void Dispose() => _cleanup.Dispose();
36+
37+
[Fact]
38+
public void CacheRemoveKey_Add_KeyIsRemoved()
39+
{
40+
ReadOnlyObservableCollection<Person> collection;
41+
_cleanup.Add(
42+
_source.Connect()
43+
.RemoveKey()
44+
.Bind(out collection)
45+
.Subscribe()
46+
);
47+
var people = _generator.Take(100).ToArray();
48+
_source.AddOrUpdate(people);
49+
50+
Assert.Equivalent(people, collection);
51+
}
52+
53+
[Fact]
54+
public void CacheRemoveKey_Filter_ItemsFilterKeyIsRemoved()
55+
{
56+
var people = _generator.Take(100).ToArray();
57+
var average = people.Average(x => x.Age);
58+
59+
ReadOnlyObservableCollection<Person> collection;
60+
_cleanup.Add(
61+
_source.Connect()
62+
.RemoveKey()
63+
.Filter(x => x.Age < average)
64+
.Bind(out collection)
65+
.Subscribe()
66+
);
67+
_source.AddOrUpdate(people);
68+
69+
Assert.Equivalent(people.Where(x => x.Age < average), collection);
70+
}
71+
72+
[Fact]
73+
public void CacheRemoveKey_AutoRefreshUpdateITems_CollectionUpdated()
74+
{
75+
ReadOnlyObservableCollection<Person> collection;
76+
_cleanup.Add(
77+
_source.Connect()
78+
.AutoRefresh(x => x.Age)
79+
.RemoveKey()
80+
.Bind(out collection)
81+
.Subscribe()
82+
);
83+
var people = _generator.Take(100).ToArray();
84+
_source.AddOrUpdate(people);
85+
86+
Assert.Equivalent(people, collection);
87+
88+
foreach (var person in people)
89+
{
90+
person.Age = person.Age + 1;
91+
}
92+
Assert.Equivalent(people, collection);
93+
}
94+
95+
}

src/DynamicData/List/Internal/Filter.Static.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ public static IObservable<IChangeSet<T>> Create(
2626

2727
var downstreamItems = new ChangeAwareList<T>();
2828
var itemsBuffer = new List<T>();
29-
3029
var downstream = source.Select(upstreamChanges =>
3130
{
3231
foreach (var change in upstreamChanges)
@@ -212,13 +211,23 @@ public static IObservable<IChangeSet<T>> Create(
212211
{
213212
var isIncluded = predicate.Invoke(change.Item.Current);
214213

215-
var itemState = upstreamItemsStates[change.Item.CurrentIndex];
216-
upstreamItemsStates[change.Item.CurrentIndex] = (
214+
var currentIndex = change.Item.CurrentIndex;
215+
// A Replace might have a negative CurrentIndex from a Refresh in RemoveKeyEnumerator
216+
if (currentIndex < 0)
217+
{
218+
var previous = upstreamItemsStates.Select(x => x.item)
219+
.IndexOfOptional(change.Item.Current)
220+
.ValueOrThrow(() => new InvalidOperationException($"Cannot find index of {typeof(T).Name} -> {change.Item.Current}. Expected to be in the list"));
221+
currentIndex = previous.Index;
222+
}
223+
var itemState = upstreamItemsStates[currentIndex];
224+
225+
upstreamItemsStates[currentIndex] = (
217226
item: change.Item.Current,
218227
isIncluded: isIncluded);
219228

220229
var downstreamIndex = (isIncluded || itemState.isIncluded)
221-
? change.Item.CurrentIndex - CountExcludedItemsBefore(change.Item.CurrentIndex)
230+
? currentIndex - CountExcludedItemsBefore(currentIndex)
222231
: -1;
223232

224233
switch (itemState.isIncluded, isIncluded)

0 commit comments

Comments
 (0)