From d164354371dbad1907ea94177d94dbb61dd54432 Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Fri, 3 Nov 2023 17:44:05 -0700 Subject: [PATCH 1/8] Basic implementation of Parent Sorted MergeManyChangeSets --- .../Cache/Internal/ChangeSetMergeTracker.cs | 39 +++++++++++++++ src/DynamicData/Cache/ObservableCacheEx.cs | 50 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs index 9adefe1a6..95d2a12ee 100644 --- a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs +++ b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs @@ -48,6 +48,31 @@ public void RemoveItems(IEnumerable> items, IObserve EmitChanges(observer); } + public void RefreshItems(IEnumerable keys, IObserver> observer) + { + var sourceCaches = _selectCaches().ToArray(); + + // Update the Published Value for each item being removed + if (keys is IList list) + { + // zero allocation enumerator + foreach (var item in EnumerableIList.Create(list)) + { + ForceEvaluate(sourceCaches, item.Value, item.Key); + } + } + else + { + foreach (var item in keys) + { + ForceEvaluate(sourceCaches, item.Key); + } + UpdateToBestValue(sources, key, cached) + } + + EmitChanges(observer); + } + public void ProcessChangeSet(IChangeSet changes, IObserver> observer) { var sourceCaches = _selectCaches().ToArray(); @@ -172,6 +197,20 @@ private void OnItemRefreshed(ChangeSetCache[] sources, TObject it } } + private void ForceEvaluate(ChangeSetCache[] sources, TKey key) + { + var cached = _resultCache.Lookup(key); + + // Received a refresh change for a key that hasn't been seen yet + // Nothing can be done, so ignore it + if (!cached.HasValue) + { + return; + } + + UpdateToBestValue(sources, key, cached); + } + private bool UpdateToBestValue(ChangeSetCache[] sources, TKey key, Optional current) { // Determine which value should be the one seen downstream diff --git a/src/DynamicData/Cache/ObservableCacheEx.cs b/src/DynamicData/Cache/ObservableCacheEx.cs index 480e969ab..f487043e4 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.cs @@ -3282,6 +3282,56 @@ public static IObservable> MergeChangeSets(source, equalityComparer, comparer, completable, scheduler).Run(); } + /// + /// Operator similiar to MergeMany except it is ChangeSet aware. It uses to transform each item in the source into a child and merges the result children together into a single stream of ChangeSets that correctly handles multiple Keys and removal of the parent items. + /// + /// The type of the object. + /// The type of the key. + /// The type of the destination. + /// The type of the destination key. + /// The Source Observable ChangeSet. + /// Factory Function used to create child changesets. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Optional instance to determine if two elements are the same. + /// The result from merging the child changesets together. + /// Parameter was null. + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, IEqualityComparer? equalityComparer = null) + where TObject : notnull + where TKey : notnull + where TDestination : notnull + where TDestinationKey : notnull + { + if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); + + return source.MergeManyChangeSets((t, _) => observableSelector(t), comparer, equalityComparer); + } + + /// + /// Operator similiar to MergeMany except it is ChangeSet aware. It uses to transform each item in the source into a child and merges the result children together into a single stream of ChangeSets that correctly handles multiple Keys and removal of the parent items. + /// + /// The type of the object. + /// The type of the key. + /// The type of the destination. + /// The type of the destination key. + /// The Source Observable ChangeSet. + /// Factory Function used to create child changesets. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Optional instance to determine if two elements are the same. + /// The result from merging the child changesets together. + /// Parameter was null. + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, IEqualityComparer? equalityComparer = null) + where TObject : notnull + where TKey : notnull + where TDestination : notnull + where TDestinationKey : notnull + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); + if (comparer == null) throw new ArgumentNullException(nameof(comparer)); + + return new MergeManyCacheChangeSetsSourceCompare(source, observableSelector, comparer, equalityComparer).Run(); + } + /// /// Operator similiar to MergeMany except it is ChangeSet aware. It uses to transform each item in the source into a child and merges the result children together into a single stream of ChangeSets that correctly handles multiple Keys and removal of the parent items. /// From cd8d699e0a3d34e70405476b4500c78da47ad902 Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Sat, 4 Nov 2023 13:41:13 -0700 Subject: [PATCH 2/8] Refactor Unit Tests --- .../Cache/MergeChangeSetsFixture.cs | 280 +++-------------- .../Cache/MergeManyCacheChangeSetsFixture.cs | 295 ++++++------------ src/DynamicData.Tests/Domain/Market.cs | 110 +++++++ src/DynamicData.Tests/Domain/MarketPrice.cs | 82 +++++ .../Utilities/FunctionalExtensions.cs | 12 + src/DynamicData.Tests/Utilities/NoOps.cs | 16 + .../Utilities/ObservableExtensions.cs | 21 ++ .../Cache/Internal/ChangeSetMergeTracker.cs | 64 ++-- .../MergeManyCacheChangeSetsSourceCompare.cs | 113 +++++++ src/DynamicData/Cache/ObservableCacheEx.cs | 102 ++++-- 10 files changed, 613 insertions(+), 482 deletions(-) create mode 100644 src/DynamicData.Tests/Domain/Market.cs create mode 100644 src/DynamicData.Tests/Domain/MarketPrice.cs create mode 100644 src/DynamicData.Tests/Utilities/FunctionalExtensions.cs create mode 100644 src/DynamicData.Tests/Utilities/NoOps.cs create mode 100644 src/DynamicData.Tests/Utilities/ObservableExtensions.cs create mode 100644 src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs diff --git a/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs b/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs index 1ea1a331a..7e76a62a4 100644 --- a/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Linq; using DynamicData.Kernel; +using DynamicData.Tests.Domain; using DynamicData.Tests.Utilities; using FluentAssertions; using Microsoft.Reactive.Testing; @@ -12,7 +11,7 @@ namespace DynamicData.Tests.Cache; -public sealed class MergeChangeSetsFixture : IDisposable +public sealed partial class MergeChangeSetsFixture : IDisposable { const int MarketCount = 101; const int PricesPerMarket = 103; @@ -28,6 +27,8 @@ public sealed class MergeChangeSetsFixture : IDisposable private readonly List _marketList = new(); + private static decimal GetRandomPrice() => MarketPrice.RandomPrice(Random, BasePrice, PriceOffset); + public MergeChangeSetsFixture() { } @@ -151,7 +152,7 @@ public void AllExistingItemsPresentInResult() { // having _marketList.AddRange(Enumerable.Range(0, MarketCount).Select(n => new Market(n))); - _marketList.ForEach((m, index) => m.AddUniquePrices(Random, index, PricesPerMarket)); + _marketList.ForEach((m, index) => m.AddUniquePrices(index, PricesPerMarket, ItemIdStride, GetRandomPrice)); // when using var pricesCache = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsObservableCache(); @@ -177,7 +178,7 @@ public void AllNewSubItemsPresentInResult() using var results = pricesCache.Connect().AsAggregator(); // when - _marketList.ForEach((m, index) => m.AddUniquePrices(Random, index, PricesPerMarket)); + _marketList.ForEach((m, index) => m.AddUniquePrices(index, PricesPerMarket, ItemIdStride, GetRandomPrice)); // then _marketList.Count.Should().Be(MarketCount); @@ -196,10 +197,10 @@ public void AllRefreshedSubItemsAreRefreshed() // having _marketList.AddRange(Enumerable.Range(0, MarketCount).Select(n => new Market(n))); using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); - _marketList.ForEach((m, index) => m.AddUniquePrices(Random, index, PricesPerMarket)); + _marketList.ForEach((m, index) => m.AddUniquePrices(index, PricesPerMarket, ItemIdStride, GetRandomPrice)); // when - _marketList.ForEach(m => m.RefreshAllPrices(Random)); + _marketList.ForEach(m => m.RefreshAllPrices(GetRandomPrice)); // then _marketList.Count.Should().Be(MarketCount); @@ -219,8 +220,8 @@ public void AnyDuplicateKeyValuesShouldBeHidden() using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); // when - _marketList[0].AddRandomPrices(Random, 0, PricesPerMarket); - _marketList[1].AddRandomPrices(Random, 0, PricesPerMarket); + _marketList[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // then _marketList.Count.Should().Be(2); @@ -237,8 +238,8 @@ public void AnyDuplicateValuesShouldBeNoOpWhenRemoved() // having _marketList.AddRange(Enumerable.Range(0, 2).Select(n => new Market(n))); using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); - _marketList[0].AddRandomPrices(Random, 0, PricesPerMarket); - _marketList[1].AddRandomPrices(Random, 0, PricesPerMarket); + _marketList[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // when _marketList[1].RemoveAllPrices(); @@ -258,8 +259,8 @@ public void AnyDuplicateValuesShouldBeUnhiddenWhenOtherIsRemoved() // having _marketList.AddRange(Enumerable.Range(0, 2).Select(n => new Market(n))); using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); - _marketList[0].AddRandomPrices(Random, 0, PricesPerMarket); - _marketList[1].AddRandomPrices(Random, 0, PricesPerMarket); + _marketList[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // when _marketList[0].RemoveAllPrices(); @@ -277,11 +278,11 @@ public void AnyDuplicateValuesShouldNotRefreshWhenHidden() // having _marketList.AddRange(Enumerable.Range(0, 2).Select(n => new Market(n))); using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); - _marketList[0].AddRandomPrices(Random, 0, PricesPerMarket); - _marketList[1].AddRandomPrices(Random, 0, PricesPerMarket); + _marketList[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // when - _marketList[1].RefreshAllPrices(Random); + _marketList[1].RefreshAllPrices(GetRandomPrice); // then _marketList.Count.Should().Be(2); @@ -296,7 +297,7 @@ public void AnyRemovedSubItemIsRemoved() // having _marketList.AddRange(Enumerable.Range(0, MarketCount).Select(n => new Market(n))); using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); - _marketList.ForEach((m, index) => m.AddUniquePrices(Random, index, PricesPerMarket)); + _marketList.ForEach((m, index) => m.AddUniquePrices(index, PricesPerMarket, ItemIdStride, GetRandomPrice)); // when _marketList.ForEach(m => m.PricesCache.Edit(updater => updater.RemoveKeys(updater.Keys.Take(RemoveCount).ToList()))); @@ -320,7 +321,7 @@ public void ComparerOnlyAddsBetterAddedValues() var others = new[] { marketLow.LatestPrices, marketHigh.LatestPrices }; using var highPriceResults = marketOriginal.LatestPrices.MergeChangeSets(others, MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = marketOriginal.LatestPrices.MergeChangeSets(others, MarketPrice.LowPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // when marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); @@ -346,7 +347,7 @@ public void ComparerOnlyAddsBetterExistingValues() var marketLow = Add(new Market(1)); var marketHigh = Add(new Market(2)); var others = new[] { marketLow.LatestPrices, marketHigh.LatestPrices }; - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); marketHigh.UpdatePrices(0, PricesPerMarket, HighestPrice); @@ -374,7 +375,7 @@ public void ComparerUpdatesToCorrectValueOnRefresh() var marketFlipFlop = Add(new Market(1)); using var highPriceResults = marketOriginal.LatestPrices.MergeChangeSets(marketFlipFlop.LatestPrices, MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = marketOriginal.LatestPrices.MergeChangeSets(marketFlipFlop.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketFlipFlop.UpdatePrices(0, PricesPerMarket, HighestPrice); // when @@ -406,7 +407,7 @@ public void ComparerUpdatesToCorrectValueOnRemove() using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.LowPriceCompare).AsAggregator(); using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.HighPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); marketHigh.UpdatePrices(0, PricesPerMarket, HighestPrice); @@ -439,7 +440,7 @@ public void ComparerUpdatesToCorrectValueOnUpdate() var marketFlipFlop = Add(new Market(1)); using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.LowPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketFlipFlop.UpdatePrices(0, PricesPerMarket, HighestPrice); // when @@ -469,7 +470,7 @@ public void ComparerOnlyUpdatesVisibleValuesOnUpdate() var marketLow = Add(new Market(1)); using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.LowPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); // when @@ -499,7 +500,7 @@ public void ComparerOnlyRefreshesVisibleValues() var marketLow = Add(new Market(1)); using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.LowPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); // when @@ -527,7 +528,7 @@ public void EnumObservableUsesTheScheduler() // having var scheduler = new TestScheduler(); _marketList.AddRange(Enumerable.Range(0, MarketCount).Select(n => new Market(n))); - _marketList.ForEach((m, index) => m.AddUniquePrices(Random, index, PricesPerMarket)); + _marketList.ForEach((m, index) => m.AddUniquePrices(index, PricesPerMarket, ItemIdStride, GetRandomPrice)); using var pricesCache = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, scheduler).AsObservableCache(); using var results = pricesCache.Connect().AsAggregator(); @@ -551,7 +552,7 @@ public void EnumObservableUsesTheSchedulerAndEmitsAll() // having var scheduler = new TestScheduler(); _marketList.AddRange(Enumerable.Range(0, MarketCount).Select(n => new Market(n))); - _marketList.ForEach((m, index) => m.AddUniquePrices(Random, index, PricesPerMarket)); + _marketList.ForEach((m, index) => m.AddUniquePrices(index, PricesPerMarket, ItemIdStride, GetRandomPrice)); using var pricesCache = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, scheduler).AsObservableCache(); using var results = pricesCache.Connect().AsAggregator(); @@ -599,7 +600,7 @@ public void EqualityComparerAndComparerWorkTogetherForUpdates() var results = market1.LatestPrices.MergeChangeSets(market2.LatestPrices, MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); var resultsTimeStamp = market1.LatestPrices.MergeChangeSets(market2.LatestPrices, MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); - market1.AddRandomPrices(Random, 0, PricesPerMarket); + market1.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); market2.UpdatePrices(0, PricesPerMarket, LowestPrice); // when @@ -629,7 +630,7 @@ public void EqualityComparerAndComparerWorkTogetherForRefreshes() var results1 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); var results2 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); - market1.AddRandomPrices(Random, 0, PricesPerMarket); + market1.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); market2.UpdatePrices(0, PricesPerMarket, LowestPrice); // Update again, but only the timestamp will change, so results1 will ignore market2.UpdatePrices(0, PricesPerMarket, LowestPrice); @@ -663,7 +664,7 @@ public void EqualityComparerAndComparerRefreshesBecomeUpdates() var results1 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); var results2 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); - market1.AddRandomPrices(Random, 0, PricesPerMarket); + market1.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); market2.UpdatePrices(0, PricesPerMarket, LowestPrice - 1); // Update again, but only the timestamp will change, so results1 will ignore market2.UpdatePrices(0, PricesPerMarket, LowestPrice - 1); @@ -671,7 +672,7 @@ public void EqualityComparerAndComparerRefreshesBecomeUpdates() // when // results1 will see this as an update because it ignored the last update // results2 will see the refreshes - market2.RefreshAllPrices(Random); + market2.RefreshAllPrices(GetRandomPrice); // then _marketList.Count.Should().Be(2); @@ -692,7 +693,7 @@ public void EqualityComparerAndComparerRefreshesBecomeUpdates() public void EveryItemVisibleWhenSequenceCompletes() { // having - var fixedMarketList = Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(Random, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket)).ToList(); + var fixedMarketList = Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(GetRandomPrice, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket)).ToList(); // when using var results = fixedMarketList.Select(m => m.LatestPrices).MergeChangeSets(completable: true).AsAggregator(); @@ -712,7 +713,7 @@ public void EveryItemVisibleWhenSequenceCompletes() public void MergedObservableCompletesWhenAllSourcesComplete(bool completeSources) { // having - var fixedMarketList = Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(Random, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket, completable: completeSources)).ToList(); + var fixedMarketList = Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(GetRandomPrice, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket, completable: completeSources)).ToList(); // when using var results = fixedMarketList.Select(m => m.LatestPrices).MergeChangeSets(completable: true).AsAggregator(); @@ -729,7 +730,7 @@ public void MergedObservableCompletesWhenAllSourcesComplete(bool completeSources public void MergedObservableRespectsCompletableFlag(bool completeSource, bool completeChildren) { // having - var fixedMarketList = Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(Random, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket, completable: completeChildren)).ToList(); + var fixedMarketList = Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(GetRandomPrice, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket, completable: completeChildren)).ToList(); // when using var results = fixedMarketList.Select(m => m.LatestPrices).MergeChangeSets(completable: completeSource).AsAggregator(); @@ -749,7 +750,7 @@ public void ObservableObservableContainsAllAddedValues() Enumerable.Range(0, MarketCount).ForEach(n => scheduler.AdvanceBy(Interval.Ticks)); // when - _marketList.ForEach((m, index) => m.AddUniquePrices(Random, index, PricesPerMarket)); + _marketList.ForEach((m, index) => m.AddUniquePrices(index, PricesPerMarket, ItemIdStride, GetRandomPrice)); // then results.Data.Count.Should().Be(MarketCount * PricesPerMarket); @@ -765,7 +766,7 @@ public void ObservableObservableContainsAllExistingValues() // having var scheduler = new TestScheduler(); _marketList.AddRange(Enumerable.Range(0, MarketCount).Select(n => new Market(n))); - _marketList.ForEach((m, index) => m.AddUniquePrices(Random, index, PricesPerMarket)); + _marketList.ForEach((m, index) => m.AddUniquePrices(index, PricesPerMarket, ItemIdStride, GetRandomPrice)); var marketObs = Observable.Interval(Interval, scheduler).Select(n => _marketList[(int)n]); using var results = marketObs.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); @@ -785,7 +786,7 @@ public void MergedObservableWillFailIfAnyChangeChangeSetFails() { // having _marketList.AddRange(Enumerable.Range(0, MarketCount).Select(n => new Market(n))); - _marketList.ForEach((m, index) => m.AddUniquePrices(Random, index, PricesPerMarket)); + _marketList.ForEach((m, index) => m.AddUniquePrices(index, PricesPerMarket, ItemIdStride, GetRandomPrice)); var expectedError = new Exception("Test exception"); var enumObservable = _marketList.Select(m => m.LatestPrices).Append(Observable.Throw>(expectedError)); @@ -804,7 +805,7 @@ public void ObservableObservableWillFailIfSourceFails() { // having _marketList.AddRange(Enumerable.Range(0, MarketCount).Select(n => new Market(n))); - _marketList.ForEach((m, index) => m.AddUniquePrices(Random, index, PricesPerMarket)); + _marketList.ForEach((m, index) => m.AddUniquePrices(index, PricesPerMarket, ItemIdStride, GetRandomPrice)); var expectedError = new Exception("Test exception"); var observables = _marketList.Select(m => m.LatestPrices).ToObservable().Concat(Observable.Throw>>(expectedError)); @@ -826,7 +827,7 @@ public void ObservableObservableWillFailIfSourceFails() public void ObservableObservableCompletesIfAndOnlyIfSourceAndAllChildrenComplete(bool completeSource, bool completeChildren) { // having - var fixedMarkets = Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(Random, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket, completable: completeChildren)); + var fixedMarkets = Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(GetRandomPrice, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket, completable: completeChildren)); var observableObservable = fixedMarkets.Select(m => m.LatestPrices).ToObservable(); if (!completeSource) { @@ -850,207 +851,4 @@ private Market Add(Market addThis) _marketList.Add(addThis); return addThis; } - - private interface IMarket - { - public string Name { get; } - - public Guid Id { get; } - - public IObservable> LatestPrices { get; } - } - - private class Market : IMarket, IDisposable - { - private readonly ISourceCache _latestPrices = new SourceCache(p => p.ItemId); - - private Market(string name, Guid id) - { - Name = name; - Id = id; - } - - public Market(Market market) : this(market.Name, market.Id) - { - } - - public Market(int name) : this($"Market #{name}", Guid.NewGuid()) - { - } - - public string Name { get; } - - public Guid Id { get; } - - public IObservable> LatestPrices => _latestPrices.Connect(); - - public ISourceCache PricesCache => _latestPrices; - - public MarketPrice CreatePrice(int itemId, decimal price) => new(itemId, price, Id); - - public Market AddRandomIdPrices(Random r, int count, int minId, int maxId) - { - _latestPrices.AddOrUpdate(Enumerable.Range(0, int.MaxValue).Select(_ => r.Next(minId, maxId)).Distinct().Take(count).Select(id => CreatePrice(id, RandomPrice(r)))); - return this; - } - - public Market AddRandomPrices(Random r, int minId, int maxId) - { - _latestPrices.AddOrUpdate(Enumerable.Range(minId, (maxId - minId)).Select(id => CreatePrice(id, RandomPrice(r)))); - return this; - } - - public Market AddUniquePrices(Random r, int section, int count) => AddRandomPrices(r, section * ItemIdStride, (section * ItemIdStride) + count); - - public Market RefreshAllPrices(decimal newPrice) - { - _latestPrices.Edit(updater => updater.Items.ForEach(cp => - { - cp.Price = newPrice; - updater.Refresh(cp); - })); - - return this; - } - - public Market RefreshAllPrices(Random r) => RefreshAllPrices(RandomPrice(r)); - - public Market RefreshPrice(int id, decimal newPrice) - { - _latestPrices.Edit(updater => updater.Lookup(id).IfHasValue(cp => - { - cp.Price = newPrice; - updater.Refresh(cp); - })); - return this; - } - - public void RemoveAllPrices() => this.With(_ => _latestPrices.Clear()); - - public void RemovePrice(int itemId) => this.With(_ => _latestPrices.Remove(itemId)); - - public Market UpdateAllPrices(decimal newPrice) => this.With(_ => _latestPrices.Edit(updater => updater.AddOrUpdate(updater.Items.Select(cp => CreatePrice(cp.ItemId, newPrice))))); - - public Market UpdatePrices(int minId, int maxId, decimal newPrice) => this.With(_ => _latestPrices.AddOrUpdate(Enumerable.Range(minId, (maxId - minId)).Select(id => CreatePrice(id, newPrice)))); - - public void Dispose() => _latestPrices.Dispose(); - } - - private static decimal RandomPrice(Random r) => BasePrice + ((decimal)r.NextDouble() * PriceOffset); - - private class MarketPrice - { - public static IEqualityComparer EqualityComparer { get; } = new CurrentPriceEqualityComparer(); - public static IEqualityComparer EqualityComparerWithTimeStamp { get; } = new TimeStampPriceEqualityComparer(); - public static IComparer HighPriceCompare { get; } = new HighestPriceComparer(); - public static IComparer LowPriceCompare { get; } = new LowestPriceComparer(); - public static IComparer LatestPriceCompare { get; } = new LatestPriceComparer(); - - private decimal _price; - - public MarketPrice(int itemId, decimal price, Guid marketId) - { - ItemId = itemId; - MarketId = marketId; - Price = price; - } - - public decimal Price - { - get => _price; - set - { - _price = value; - TimeStamp = DateTimeOffset.UtcNow; - } - } - - public DateTimeOffset TimeStamp { get; private set; } - - public Guid MarketId { get; } - - public int ItemId { get; } - - public override string ToString() => $"{ItemId:D5} - {Price:c} ({MarketId}) [{TimeStamp:HH:mm:ss.fffffff}]"; - - private class CurrentPriceEqualityComparer : IEqualityComparer - { - public virtual bool Equals([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) => x.MarketId.Equals(x.MarketId) && (x.ItemId == y.ItemId) && (x.Price == y.Price); - public int GetHashCode([DisallowNull] MarketPrice obj) => throw new NotImplementedException(); - } - - private class TimeStampPriceEqualityComparer : CurrentPriceEqualityComparer, IEqualityComparer - { - public override bool Equals([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) => base.Equals(x, y) && (x.TimeStamp == y.TimeStamp); - } - - private class LowestPriceComparer : IComparer - { - public int Compare([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) - { - Debug.Assert(x.ItemId == y.ItemId); - return x.Price.CompareTo(y.Price); - } - } - - private class HighestPriceComparer : IComparer - { - public int Compare([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) - { - Debug.Assert(x.ItemId == y.ItemId); - return y.Price.CompareTo(x.Price); - } - } - - private class LatestPriceComparer : IComparer - { - public int Compare([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) - { - Debug.Assert(x.ItemId == y.ItemId); - return y.TimeStamp.CompareTo(x.TimeStamp); - } - } - } - - private class FixedMarket : IMarket - { - public FixedMarket(Random r, int minId, int maxId, bool completable = true) - { - Id = Guid.NewGuid(); - LatestPrices = Enumerable.Range(minId, maxId - minId) - .Select(id => new MarketPrice(id, RandomPrice(r), Id)) - .AsObservableChangeSet(cp => cp.ItemId, completable: completable); - } - - public IObservable> LatestPrices { get; } - - public string Name => Id.ToString("B"); - - public Guid Id { get; } - } - - class NoOpComparer : IComparer - { - public int Compare(T x, T y) => throw new NotImplementedException(); - } - - class NoOpEqualityComparer : IEqualityComparer - { - public bool Equals(T x, T y) => throw new NotImplementedException(); - public int GetHashCode([DisallowNull] T obj) => throw new NotImplementedException(); - } -} - -internal static class Extensions -{ - public static T With(this T item, Action action) - { - action(item); - return item; - } - - public static IObservable ForceFail(this IObservable source, int count, Exception? e) => - (e is not null) - ? source.Take(count).Concat(Observable.Throw(e)) - : source; } diff --git a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs index 0b083f01d..b13097968 100644 --- a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Reactive.Linq; using DynamicData.Kernel; +using DynamicData.Tests.Domain; +using DynamicData.Tests.Utilities; using FluentAssertions; using Xunit; @@ -25,6 +27,8 @@ public sealed class MergeManyCacheChangeSetsFixture : IDisposable private static readonly Random Random = new Random(0x21123737); + private static decimal GetRandomPrice() => MarketPrice.RandomPrice(Random, BasePrice, PriceOffset); + private readonly ISourceCache _marketCache = new SourceCache(p => p.Id); private readonly ChangeSetAggregator _marketCacheResults; @@ -34,6 +38,75 @@ public MergeManyCacheChangeSetsFixture() _marketCacheResults = _marketCache.Connect().AsAggregator(); } + [Fact] + public void NullChecks() + { + // having + var emptyChangeSetObs = Observable.Empty>(); + var nullChangeSetObs = (IObservable>)null!; + var emptyChildChangeSetObs = Observable.Empty>(); + var emptySelector = new Func>>(i => emptyChildChangeSetObs); + var emptyKeySelector = new Func>>((i, key) => emptyChildChangeSetObs); + var nullSelector = (Func>>)null!; + var nullKeySelector = (Func>>)null!; + var nullParentComparer = (IComparer)null!; + var emptyParentComparer = new NoOpComparer() as IComparer; + var nullChildComparer = (IComparer)null!; + var emptyChildComparer = new NoOpComparer() as IComparer; + var nullEqualityComparer = (IEqualityComparer)null!; + var emptyEqualityComparer = new NoOpEqualityComparer() as IEqualityComparer; + + // when + var actionDefault1 = () => emptyChangeSetObs.MergeManyChangeSets(nullSelector); + var actionDefault2a = () => nullChangeSetObs.MergeManyChangeSets(emptyKeySelector); + var actionDefault2b = () => emptyChangeSetObs.MergeManyChangeSets(nullKeySelector); + var actionChildCompare1 = () => emptyChangeSetObs.MergeManyChangeSets(nullSelector, comparer: emptyChildComparer); + var actionChildCompare2a = () => nullChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: emptyChildComparer); + var actionChildCompare2b = () => emptyChangeSetObs.MergeManyChangeSets(nullKeySelector, comparer: emptyChildComparer); + var actionChildCompare2c = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: nullChildComparer); + var actionParentCompare1 = () => emptyChangeSetObs.MergeManyChangeSets(nullSelector, comparer: emptyParentComparer); + var actionParentCompareKey1a = () => nullChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: emptyParentComparer); + var actionParentCompareKey1b = () => emptyChangeSetObs.MergeManyChangeSets(nullKeySelector, comparer: emptyParentComparer); + var actionParentCompareKey1c = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: nullParentComparer); + var actionParentCompare2 = () => emptyChangeSetObs.MergeManyChangeSets(nullSelector, comparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); + var actionParentCompareKey2a = () => nullChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); + var actionParentCompareKey2b = () => emptyChangeSetObs.MergeManyChangeSets(nullKeySelector, comparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); + var actionParentCompareKey2c = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: nullParentComparer, equalityComparer: emptyEqualityComparer); + var actionParentCompareKey2d = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: emptyParentComparer, equalityComparer: nullEqualityComparer); + + // then + emptyChangeSetObs.Should().NotBeNull(); + emptyChildChangeSetObs.Should().NotBeNull(); + emptyChildComparer.Should().NotBeNull(); + emptyEqualityComparer.Should().NotBeNull(); + emptyKeySelector.Should().NotBeNull(); + emptyParentComparer.Should().NotBeNull(); + emptySelector.Should().NotBeNull(); + nullChangeSetObs.Should().BeNull(); + nullChildComparer.Should().BeNull(); + nullEqualityComparer.Should().BeNull(); + nullKeySelector.Should().BeNull(); + nullParentComparer.Should().BeNull(); + nullSelector.Should().BeNull(); + + actionDefault1.Should().Throw(); + actionDefault2a.Should().Throw(); + actionDefault2b.Should().Throw(); + actionChildCompare1.Should().Throw(); + actionChildCompare2a.Should().Throw(); + actionChildCompare2b.Should().Throw(); + actionChildCompare2c.Should().Throw(); + actionParentCompare1.Should().Throw(); + actionParentCompareKey1a.Should().Throw(); + actionParentCompareKey1b.Should().Throw(); + actionParentCompareKey1c.Should().Throw(); + actionParentCompare2.Should().Throw(); + actionParentCompareKey2a.Should().Throw(); + actionParentCompareKey2b.Should().Throw(); + actionParentCompareKey2c.Should().Throw(); + actionParentCompareKey2d.Should().Throw(); + } + [Fact] public void AbleToInvokeFactory() { @@ -52,11 +125,6 @@ IObservable> factory(IMarket m) // then _marketCacheResults.Data.Count.Should().Be(1); invoked.Should().BeTrue(); - Assert.Throws(() => _marketCache.Connect().MergeManyChangeSets((Func>>)null!, comparer: null!)); - Assert.Throws(() => _marketCache.Connect().MergeManyChangeSets(_ => Observable.Return(ChangeSet.Empty), comparer: null!)); - Assert.Throws(() => _marketCache.Connect().MergeManyChangeSets((Func>>)null!, null!, null!)); - Assert.Throws(() => ObservableCacheEx.MergeManyChangeSets(null!, (Func>>)null!, comparer: null!)); - Assert.Throws(() => ObservableCacheEx.MergeManyChangeSets(null!, (Func>>)null!, null!, null!)); } [Fact] @@ -77,11 +145,6 @@ IObservable> factory(IMarket m, Guid g) // then _marketCacheResults.Data.Count.Should().Be(1); invoked.Should().BeTrue(); - Assert.Throws(() => _marketCache.Connect().MergeManyChangeSets((Func>>)null!, comparer: null!)); - Assert.Throws(() => _marketCache.Connect().MergeManyChangeSets((_, _) => Observable.Return(ChangeSet.Empty), comparer: null!)); - Assert.Throws(() => _marketCache.Connect().MergeManyChangeSets((Func>>)null!, null!, null!)); - Assert.Throws(() => ObservableCacheEx.MergeManyChangeSets(null!, (Func>>)null!, comparer: null!)); - Assert.Throws(() => ObservableCacheEx.MergeManyChangeSets(null!, (Func>>)null!, null!, null!)); } [Fact] @@ -90,7 +153,7 @@ public void AllExistingSubItemsPresentInResult() // having var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(Random, m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when _marketCache.AddOrUpdate(markets); @@ -114,7 +177,7 @@ public void AllNewSubItemsPresentInResult() _marketCache.AddOrUpdate(markets); // when - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(Random, m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // then _marketCacheResults.Data.Count.Should().Be(MarketCount); @@ -133,10 +196,10 @@ public void AllRefreshedSubItemsAreRefreshed() var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(Random, m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when - markets.ForEach(m => m.RefreshAllPrices(Random)); + markets.ForEach(m => m.RefreshAllPrices(GetRandomPrice)); // then _marketCacheResults.Data.Count.Should().Be(MarketCount); @@ -157,8 +220,8 @@ public void AnyDuplicateKeyValuesShouldBeHidden() _marketCache.AddOrUpdate(markets); // when - markets[0].AddRandomPrices(Random, 0, PricesPerMarket); - markets[1].AddRandomPrices(Random, 0, PricesPerMarket); + markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // then _marketCacheResults.Data.Count.Should().Be(2); @@ -176,8 +239,8 @@ public void AnyDuplicateValuesShouldBeNoOpWhenRemoved() var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(Random, 0, PricesPerMarket); - markets[1].AddRandomPrices(Random, 0, PricesPerMarket); + markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // when markets[1].RemoveAllPrices(); @@ -198,8 +261,8 @@ public void AnyDuplicateValuesShouldBeUnhiddenWhenOtherIsRemoved() var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(Random, 0, PricesPerMarket); - markets[1].AddRandomPrices(Random, 0, PricesPerMarket); + markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // when _marketCache.Remove(markets[0]); @@ -219,11 +282,11 @@ public void AnyDuplicateValuesShouldNotRefreshWhenHidden() var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(Random, 0, PricesPerMarket); - markets[1].AddRandomPrices(Random, 0, PricesPerMarket); + markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // when - markets[1].RefreshAllPrices(Random); + markets[1].RefreshAllPrices(GetRandomPrice); // then _marketCacheResults.Data.Count.Should().Be(2); @@ -239,7 +302,7 @@ public void AnyRemovedSubItemIsRemoved() var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(Random, m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when markets.ForEach(m => m.PricesCache.Edit(updater => updater.RemoveKeys(updater.Keys.Take(RemoveCount)))); @@ -260,7 +323,7 @@ public void AnySourceItemRemovedRemovesAllSourceValues() var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(Random, m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when _marketCache.Edit(updater => updater.RemoveKeys(updater.Keys.Take(RemoveCount))); @@ -278,10 +341,10 @@ public void ChangingSourceByUpdateRemovesPreviousAndAddsNewValues() // having using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); var market = new Market(0); - market.AddRandomPrices(Random, 0, PricesPerMarket * 2); + market.AddRandomPrices(0, PricesPerMarket * 2, GetRandomPrice); _marketCache.AddOrUpdate(market); var updatedMarket = new Market(market); - updatedMarket.AddRandomPrices(Random, PricesPerMarket, PricesPerMarket * 3); + updatedMarket.AddRandomPrices(PricesPerMarket, PricesPerMarket * 3, GetRandomPrice); // when _marketCache.AddOrUpdate(updatedMarket); @@ -304,7 +367,7 @@ public void ComparerOnlyAddsBetterAddedValues() var marketOriginal = new Market(0); var marketLow = new Market(1); var marketHigh = new Market(2); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); _marketCache.AddOrUpdate(marketHigh); @@ -334,7 +397,7 @@ public void ComparerOnlyAddsBetterExistingValues() var marketOriginal = new Market(0); var marketLow = new Market(1); var marketHigh = new Market(2); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); marketHigh.UpdatePrices(0, PricesPerMarket, HighestPrice); @@ -364,7 +427,7 @@ public void ComparerOnlyAddsBetterValuesOnSourceUpdate() var marketOriginal = new Market(0); var marketLow = new Market(1); var marketLowLow = new Market(marketLow); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); marketLowLow.UpdatePrices(0, PricesPerMarket, LowestPrice - 1); _marketCache.AddOrUpdate(marketOriginal); @@ -395,7 +458,7 @@ public void ComparerUpdatesToCorrectValueOnRefresh() using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); var marketOriginal = new Market(0); var marketFlipFlop = new Market(1); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketFlipFlop.UpdatePrices(0, PricesPerMarket, HighestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketFlipFlop); @@ -429,7 +492,7 @@ public void ComparerUpdatesToCorrectValueOnRemove() var marketOriginal = new Market(0); var marketLow = new Market(1); var marketHigh = new Market(2); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); _marketCache.AddOrUpdate(marketHigh); @@ -465,7 +528,7 @@ public void ComparerUpdatesToCorrectValueOnUpdate() using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); var marketOriginal = new Market(0); var marketFlipFlop = new Market(1); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketFlipFlop.UpdatePrices(0, PricesPerMarket, HighestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketFlipFlop); @@ -497,7 +560,7 @@ public void ComparerOnlyUpdatesVisibleValuesOnUpdate() using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); var marketOriginal = new Market(0); var marketLow = new Market(1); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); @@ -529,7 +592,7 @@ public void ComparerOnlyRefreshesVisibleValues() using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer, MarketPrice.LowPriceCompare).AsAggregator(); var marketOriginal = new Market(0); var marketLow = new Market(1); - marketOriginal.AddRandomPrices(Random, 0, PricesPerMarket); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); @@ -579,7 +642,7 @@ public void EqualityComparerHidesUpdatesWithoutChanges() public void EveryItemVisibleWhenSequenceCompletes() { // having - _marketCache.AddOrUpdate(Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(Random, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket))); + _marketCache.AddOrUpdate(Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(GetRandomPrice, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket))); // when using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices).AsAggregator(); @@ -601,7 +664,7 @@ public void EveryItemVisibleWhenSequenceCompletes() public void MergedObservableCompletesOnlyWhenSourceAndAllChildrenComplete(bool completeSource, bool completeChildren) { // having - _marketCache.AddOrUpdate(Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(Random, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket, completable: completeChildren))); + _marketCache.AddOrUpdate(Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(GetRandomPrice, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket, completable: completeChildren))); var hasSourceSequenceCompleted = false; var hasMergedSequenceCompleted = false; @@ -651,162 +714,4 @@ private void DisposeMarkets() _marketCache.Dispose(); _marketCache.Clear(); } - - private interface IMarket - { - public string Name { get; } - - public Guid Id { get; } - - public IObservable> LatestPrices { get; } - } - - private class Market : IMarket, IDisposable - { - private readonly ISourceCache _latestPrices = new SourceCache(p => p.ItemId); - - private Market(string name, Guid id) - { - Name = name; - Id = id; - } - - public Market(Market market) : this(market.Name, market.Id) - { - } - - public Market(int name) : this($"Market #{name}", Guid.NewGuid()) - { - } - - public string Name { get; } - - public Guid Id { get; } - - public IObservable> LatestPrices => _latestPrices.Connect(); - - public ISourceCache PricesCache => _latestPrices; - - public MarketPrice CreatePrice(int itemId, decimal price) => new (itemId, price, Id); - - public void AddRandomIdPrices(Random r, int count, int minId, int maxId) => - _latestPrices.AddOrUpdate(Enumerable.Range(0, int.MaxValue).Select(_ => r.Next(minId, maxId)).Distinct().Take(count).Select(id => CreatePrice(id, RandomPrice(r)))); - - public void AddRandomPrices(Random r, int minId, int maxId) => - _latestPrices.AddOrUpdate(Enumerable.Range(minId, (maxId - minId)).Select(id => CreatePrice(id, RandomPrice(r)))); - - public void RefreshAllPrices(decimal newPrice) => - _latestPrices.Edit(updater => updater.Items.ForEach(cp => - { - cp.Price = newPrice; - updater.Refresh(cp); - })); - - public void RefreshAllPrices(Random r) => RefreshAllPrices(RandomPrice(r)); - - public void RefreshPrice(int id, decimal newPrice) => - _latestPrices.Edit(updater => updater.Lookup(id).IfHasValue(cp => - { - cp.Price = newPrice; - updater.Refresh(cp); - })); - - public void RemoveAllPrices() => _latestPrices.Clear(); - - public void RemovePrice(int itemId) => _latestPrices.Remove(itemId); - - public void UpdateAllPrices(decimal newPrice) => - _latestPrices.Edit(updater => updater.AddOrUpdate(updater.Items.Select(cp => CreatePrice(cp.ItemId, newPrice)))); - - public void UpdatePrices(int minId, int maxId, decimal newPrice) => - _latestPrices.AddOrUpdate(Enumerable.Range(minId, (maxId - minId)).Select(id => CreatePrice(id, newPrice))); - - public void Dispose() => _latestPrices.Dispose(); - } - - private static decimal RandomPrice(Random r) => BasePrice + ((decimal)r.NextDouble() * PriceOffset); - - private class MarketPrice - { - public static IEqualityComparer EqualityComparer { get; } = new CurrentPriceEqualityComparer(); - public static IComparer HighPriceCompare { get; } = new HighestPriceComparer(); - public static IComparer LowPriceCompare { get; } = new LowestPriceComparer(); - public static IComparer LatestPriceCompare { get; } = new LatestPriceComparer(); - - private decimal _price; - - public MarketPrice(int itemId, decimal price, Guid marketId) - { - ItemId = itemId; - MarketId = marketId; - Price = price; - } - - public decimal Price - { - get => _price; - set - { - _price = value; - TimeStamp = DateTimeOffset.UtcNow; - } - } - - public DateTimeOffset TimeStamp { get; private set; } - - public Guid MarketId { get; } - - public int ItemId { get; } - - private class CurrentPriceEqualityComparer : IEqualityComparer - { - public bool Equals([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) => x.MarketId.Equals(x.MarketId) && (x.ItemId == y.ItemId) && (x.Price == y.Price); - public int GetHashCode([DisallowNull] MarketPrice obj) => throw new NotImplementedException(); - } - - private class LowestPriceComparer : IComparer - { - public int Compare([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) - { - Debug.Assert(x.ItemId == y.ItemId); - return x.Price.CompareTo(y.Price); - } - } - - private class HighestPriceComparer : IComparer - { - public int Compare([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) - { - Debug.Assert(x.ItemId == y.ItemId); - return y.Price.CompareTo(x.Price); - } - } - - private class LatestPriceComparer : IComparer - { - public int Compare([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) - { - Debug.Assert(x.ItemId == y.ItemId); - return x.TimeStamp.CompareTo(y.TimeStamp); - } - } - } - - private class FixedMarket : IMarket - { - public FixedMarket(Random r, int minId, int maxId, bool completable = true) - { - Id = Guid.NewGuid(); - LatestPrices = Enumerable.Range(minId, maxId - minId) - .Select(id => new MarketPrice(id, RandomPrice(r), Id)) - .AsObservableChangeSet(cp => cp.ItemId, completable: completable); - } - - public IObservable> LatestPrices { get; } - - public string Name => Id.ToString("B"); - - public Guid Id { get; } - } - } diff --git a/src/DynamicData.Tests/Domain/Market.cs b/src/DynamicData.Tests/Domain/Market.cs new file mode 100644 index 000000000..b13b33a33 --- /dev/null +++ b/src/DynamicData.Tests/Domain/Market.cs @@ -0,0 +1,110 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using DynamicData.Kernel; +using DynamicData.Tests.Utilities; + +namespace DynamicData.Tests.Domain; + +internal interface IMarket +{ + public string Name { get; } + + public Guid Id { get; } + + public IObservable> LatestPrices { get; } +} + +internal class Market : IMarket, IDisposable +{ + private readonly ISourceCache _latestPrices = new SourceCache(p => p.ItemId); + + private Market(string name, Guid id) + { + Name = name; + Id = id; + } + + public Market(Market market) : this(market.Name, market.Id) + { + } + + public Market(int name) : this($"Market #{name}", Guid.NewGuid()) + { + } + + public string Name { get; } + + public Guid Id { get; } + + public IObservable> LatestPrices => _latestPrices.Connect(); + + public ISourceCache PricesCache => _latestPrices; + + public MarketPrice CreatePrice(int itemId, decimal price) => new(itemId, price, Id); + + public Market AddRandomIdPrices(Random r, int count, int minId, int maxId, Func randPrices) + { + _latestPrices.AddOrUpdate(Enumerable.Range(0, int.MaxValue).Select(_ => r.Next(minId, maxId)).Distinct().Take(count).Select(id => CreatePrice(id, randPrices()))); + return this; + } + + public Market AddRandomPrices(int minId, int maxId, Func randPrices) + { + _latestPrices.AddOrUpdate(Enumerable.Range(minId, maxId - minId).Select(id => CreatePrice(id, randPrices()))); + return this; + } + + public Market AddUniquePrices(int section, int count, int stride, Func randPrices) => AddRandomPrices(section * stride, section * stride + count, randPrices); + + public Market RefreshAllPrices(decimal newPrice) + { + _latestPrices.Edit(updater => updater.Items.ForEach(cp => + { + cp.Price = newPrice; + updater.Refresh(cp); + })); + + return this; + } + + public Market RefreshAllPrices(Func randPrices) => RefreshAllPrices(randPrices()); + + public Market RefreshPrice(int id, decimal newPrice) + { + _latestPrices.Edit(updater => updater.Lookup(id).IfHasValue(cp => + { + cp.Price = newPrice; + updater.Refresh(cp); + })); + return this; + } + + public void RemoveAllPrices() => this.With(_ => _latestPrices.Clear()); + + public void RemovePrice(int itemId) => this.With(_ => _latestPrices.Remove(itemId)); + + public Market UpdateAllPrices(decimal newPrice) => this.With(_ => _latestPrices.Edit(updater => updater.AddOrUpdate(updater.Items.Select(cp => CreatePrice(cp.ItemId, newPrice))))); + + public Market UpdatePrices(int minId, int maxId, decimal newPrice) => this.With(_ => _latestPrices.AddOrUpdate(Enumerable.Range(minId, maxId - minId).Select(id => CreatePrice(id, newPrice)))); + + public void Dispose() => _latestPrices.Dispose(); +} + + +internal class FixedMarket : IMarket +{ + public FixedMarket(Func getPrice, int minId, int maxId, bool completable = true) + { + Id = Guid.NewGuid(); + LatestPrices = Enumerable.Range(minId, maxId - minId) + .Select(id => new MarketPrice(id, getPrice(), Id)) + .AsObservableChangeSet(cp => cp.ItemId, completable: completable); + } + + public IObservable> LatestPrices { get; } + + public string Name => Id.ToString("B"); + + public Guid Id { get; } +} diff --git a/src/DynamicData.Tests/Domain/MarketPrice.cs b/src/DynamicData.Tests/Domain/MarketPrice.cs new file mode 100644 index 000000000..05dfaa97b --- /dev/null +++ b/src/DynamicData.Tests/Domain/MarketPrice.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace DynamicData.Tests.Domain; + +internal class MarketPrice +{ + public static IEqualityComparer EqualityComparer { get; } = new CurrentPriceEqualityComparer(); + public static IEqualityComparer EqualityComparerWithTimeStamp { get; } = new TimeStampPriceEqualityComparer(); + public static IComparer HighPriceCompare { get; } = new HighestPriceComparer(); + public static IComparer LowPriceCompare { get; } = new LowestPriceComparer(); + public static IComparer LatestPriceCompare { get; } = new LatestPriceComparer(); + + private decimal _price; + + public MarketPrice(int itemId, decimal price, Guid marketId) + { + ItemId = itemId; + MarketId = marketId; + Price = price; + } + + public decimal Price + { + get => _price; + set + { + _price = value; + TimeStamp = DateTimeOffset.UtcNow; + } + } + + public DateTimeOffset TimeStamp { get; private set; } + + public Guid MarketId { get; } + + public int ItemId { get; } + + public override string ToString() => $"{ItemId:D5} - {Price:c} ({MarketId}) [{TimeStamp:HH:mm:ss.fffffff}]"; + + public static decimal RandomPrice(Random r, decimal basePrice, decimal offset) => basePrice + (decimal)r.NextDouble() * offset; + + private class CurrentPriceEqualityComparer : IEqualityComparer + { + public virtual bool Equals([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) => x.MarketId.Equals(x.MarketId) && x.ItemId == y.ItemId && x.Price == y.Price; + public int GetHashCode([DisallowNull] MarketPrice obj) => throw new NotImplementedException(); + } + + private class TimeStampPriceEqualityComparer : CurrentPriceEqualityComparer, IEqualityComparer + { + public override bool Equals([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) => base.Equals(x, y) && x.TimeStamp == y.TimeStamp; + } + + private class LowestPriceComparer : IComparer + { + public int Compare([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) + { + Debug.Assert(x.ItemId == y.ItemId); + return x.Price.CompareTo(y.Price); + } + } + + private class HighestPriceComparer : IComparer + { + public int Compare([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) + { + Debug.Assert(x.ItemId == y.ItemId); + return y.Price.CompareTo(x.Price); + } + } + + private class LatestPriceComparer : IComparer + { + public int Compare([DisallowNull] MarketPrice x, [DisallowNull] MarketPrice y) + { + Debug.Assert(x.ItemId == y.ItemId); + return y.TimeStamp.CompareTo(x.TimeStamp); + } + } +} diff --git a/src/DynamicData.Tests/Utilities/FunctionalExtensions.cs b/src/DynamicData.Tests/Utilities/FunctionalExtensions.cs new file mode 100644 index 000000000..cb3c819cf --- /dev/null +++ b/src/DynamicData.Tests/Utilities/FunctionalExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace DynamicData.Tests.Utilities; + +internal static class FunctionalExtensions +{ + public static T With(this T item, Action action) + { + action(item); + return item; + } +} diff --git a/src/DynamicData.Tests/Utilities/NoOps.cs b/src/DynamicData.Tests/Utilities/NoOps.cs new file mode 100644 index 000000000..f57025517 --- /dev/null +++ b/src/DynamicData.Tests/Utilities/NoOps.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace DynamicData.Tests.Utilities; + +internal class NoOpComparer : IComparer +{ + public int Compare(T x, T y) => throw new NotImplementedException(); +} + +internal class NoOpEqualityComparer : IEqualityComparer +{ + public bool Equals(T x, T y) => throw new NotImplementedException(); + public int GetHashCode([DisallowNull] T obj) => throw new NotImplementedException(); +} diff --git a/src/DynamicData.Tests/Utilities/ObservableExtensions.cs b/src/DynamicData.Tests/Utilities/ObservableExtensions.cs new file mode 100644 index 000000000..f1f622c38 --- /dev/null +++ b/src/DynamicData.Tests/Utilities/ObservableExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using System.Reactive.Linq; + +namespace DynamicData.Tests.Utilities; + +internal static class ObservableExtensions +{ + /// + /// Forces the given observable to fail after the specified number events if an exception is provided. + /// + /// Observable type. + /// Source Observable. + /// Number of events before failing. + /// Exception to fail with. + /// The new Observable. + public static IObservable ForceFail(this IObservable source, int count, Exception? e) => + e is not null + ? source.Take(count).Concat(Observable.Throw(e)) + : source; +} diff --git a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs index 95d2a12ee..244e3a7c0 100644 --- a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs +++ b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs @@ -56,18 +56,17 @@ public void RefreshItems(IEnumerable keys, IObserver list) { // zero allocation enumerator - foreach (var item in EnumerableIList.Create(list)) + foreach (var key in EnumerableIList.Create(list)) { - ForceEvaluate(sourceCaches, item.Value, item.Key); + ForceEvaluate(sourceCaches, key); } } else { - foreach (var item in keys) + foreach (var key in keys) { - ForceEvaluate(sourceCaches, item.Key); + ForceEvaluate(sourceCaches, key); } - UpdateToBestValue(sources, key, cached) } EmitChanges(observer); @@ -150,10 +149,13 @@ private void OnItemUpdated(ChangeSetCache[] sources, TObject item return; } + // If the Previous value is missing or is the same as the current value + bool isUpdatingCurrent = !prev.HasValue || CheckEquality(prev.Value, cached.Value); + if (_comparer is null) { - // If the current value (or there is no way to tell) is being replaced by a different value - if ((!prev.HasValue || CheckEquality(prev.Value, cached.Value)) && !CheckEquality(item, cached.Value)) + // If not using the comparer and the current value is being replaced by a different value + if (isUpdatingCurrent && !CheckEquality(item, cached.Value)) { // Update to the new value _resultCache.AddOrUpdate(item, key); @@ -161,14 +163,16 @@ private void OnItemUpdated(ChangeSetCache[] sources, TObject item } else { - // The current value is being replaced (or there is no way to tell), so do a full update to select the best one from all the choices - if (!prev.HasValue || CheckEquality(prev.Value, cached.Value)) + // If using the comparer and the current value is one being updated + if (isUpdatingCurrent) { + // The known best value has been replaced, so pick a new one from all the choices UpdateToBestValue(sources, key, cached); } else { - // If the current value isn't being replaced, check to see if the replacement value is better than the current one + // If the current value isn't being replaced, its only required to check to see if the + // new value is better than the current one if (ShouldReplace(item, cached.Value)) { _resultCache.AddOrUpdate(item, key); @@ -181,19 +185,33 @@ private void OnItemRefreshed(ChangeSetCache[] sources, TObject it { var cached = _resultCache.Lookup(key); - // Received a refresh change for a key that hasn't been seen yet - // Nothing can be done, so ignore it - if (!cached.HasValue) + // Only proceed if the key has a current value + if (cached.HasValue) { - return; - } + // If the refreshed value is the current one + if (ReferenceEquals(cached.Value, item)) + { + // When using a compare and the current value has changed, so do a full search for + // the best value to make sure the current choice is still the best choice + if ((_comparer is not null) && UpdateToBestValue(sources, key, cached)) + { + // A new value was choosen, so there's nothing left to do + return; + } - // In the sorting case, a refresh requires doing a full update because any change could alter what the best value is - // If we don't care about sorting OR if we do care, but re-selecting the best value didn't change anything - // AND the current value is the exact one being refreshed, then emit the refresh downstream - if (((_comparer is null) || !UpdateToBestValue(sources, key, cached)) && ReferenceEquals(cached.Value, item)) - { - _resultCache.Refresh(key); + // The current one is still the best choice and it was refreshed, so + // emit the Refresh downstream so consumers will see it. + _resultCache.Refresh(key); + } + else + { + // If the current value isn't being refreshed and using a comparer, + // check if the refreshed item is now a better choice + if ((_comparer is not null) && ShouldReplace(item, cached.Value)) + { + _resultCache.AddOrUpdate(item, key); + } + } } } @@ -214,7 +232,7 @@ private void ForceEvaluate(ChangeSetCache[] sources, TKey key) private bool UpdateToBestValue(ChangeSetCache[] sources, TKey key, Optional current) { // Determine which value should be the one seen downstream - var candidate = SelectValue(sources, key); + var candidate = LookupBestValue(sources, key); if (candidate.HasValue) { // If there isn't a current value @@ -240,7 +258,7 @@ private bool UpdateToBestValue(ChangeSetCache[] sources, TKey key return true; } - private Optional SelectValue(ChangeSetCache[] sources, TKey key) + private Optional LookupBestValue(ChangeSetCache[] sources, TKey key) { if (sources.Length == 0) { diff --git a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs new file mode 100644 index 000000000..40dd2fba1 --- /dev/null +++ b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs @@ -0,0 +1,113 @@ +// Copyright (c) 2011-2023 Roland Pheasant. All rights reserved. +// Roland Pheasant licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Reactive.Disposables; +using System.Reactive.Linq; + +namespace DynamicData.Cache.Internal; + +/// +/// Alternate version of MergeManyCacheChangeSets that uses a Comparer of the source, not the destination type +/// So that items from the most important source go into the resulting changeset. +/// +internal sealed class MergeManyCacheChangeSetsSourceCompare + where TObject : notnull + where TKey : notnull + where TDestination : notnull + where TDestinationKey : notnull +{ + private readonly IObservable> _source; + + private readonly Func>> _changeSetSelector; + + private readonly IComparer? _comparer; + + private readonly IEqualityComparer? _equalityComparer; + + private readonly bool _reevalOnRefresh; + + public MergeManyCacheChangeSetsSourceCompare(IObservable> source, Func>> selector, IComparer comparer, IEqualityComparer? equalityComparer, bool reevalOnRefresh = false) + { + _source = source; + _changeSetSelector = (obj, key) => selector(obj, key).Transform(dest => new ParentChildEntry(obj, dest)); + _comparer = new ParentChildCompare(comparer); + _equalityComparer = (equalityComparer != null) ? new ParentChildEqualityCompare(equalityComparer) : null; + _reevalOnRefresh = reevalOnRefresh; + } + + public IObservable> Run() + { + return Observable.Create>( + observer => + { + var locker = new object(); + + // Transform to an observable cache of merge containers. + var sourceCacheOfCaches = _source + .Transform((obj, key) => new ChangeSetCache(_changeSetSelector(obj, key))) + .Synchronize(locker) + .AsObservableCache(); + + var shared = sourceCacheOfCaches.Connect().Publish(); + + // this is manages all of the changes + var changeTracker = new ChangeSetMergeTracker(() => sourceCacheOfCaches.Items, _comparer, _equalityComparer); + + // merge the items back together + var allChanges = shared.MergeMany(mc => mc.Source) + .Synchronize(locker) + .Subscribe( + changes => changeTracker.ProcessChangeSet(changes, observer), + observer.OnError, + observer.OnCompleted); + + // when a source item is removed, all of its sub-items need to be removed + var removedItems = shared + .OnItemRemoved(mc => changeTracker.RemoveItems(mc.Cache.KeyValues, observer)) + .OnItemUpdated((_, prev) => changeTracker.RemoveItems(prev.Cache.KeyValues, observer)) + .Subscribe(); + + // If requested, when the source sees a refresh event, re-evaluate all the keys associated with that source because the priority may have changed + // Because the comparison is based on the parent, which has just been refreshed. + var refreshItems = _reevalOnRefresh + ? shared.OnItemRefreshed(mc => changeTracker.RefreshItems(mc.Cache.Keys, observer)).Subscribe() + : Disposable.Empty; + + return new CompositeDisposable(sourceCacheOfCaches, allChanges, removedItems, refreshItems, shared.Connect()); + }).Transform(entry => entry.Child); + } + + private readonly struct ParentChildEntry + { + public ParentChildEntry(TObject parent, TDestination child) + { + Parent = parent; + Child = child; + } + + public TObject Parent { get; } + + public TDestination Child { get; } + } + + private class ParentChildCompare : IComparer + { + private readonly IComparer _comparer; + + public ParentChildCompare(IComparer comparer) => _comparer = comparer; + + public int Compare(ParentChildEntry x, ParentChildEntry y) => _comparer.Compare(x.Parent, y.Parent); + } + + private class ParentChildEqualityCompare : IEqualityComparer + { + private readonly IEqualityComparer _comparer; + + public ParentChildEqualityCompare(IEqualityComparer comparer) => _comparer = comparer; + + public bool Equals(ParentChildEntry x, ParentChildEntry y) => _comparer.Equals(x.Child, y.Child); + + public int GetHashCode(ParentChildEntry obj) => _comparer.GetHashCode(obj.Child); + } +} diff --git a/src/DynamicData/Cache/ObservableCacheEx.cs b/src/DynamicData/Cache/ObservableCacheEx.cs index f487043e4..cfeac60c6 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.cs @@ -3291,11 +3291,10 @@ public static IObservable> MergeChangeSetsThe type of the destination key. /// The Source Observable ChangeSet. /// Factory Function used to create child changesets. - /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. - /// Optional instance to determine if two elements are the same. + /// instance to determine which element to emit if the same key is emitted from multiple child changesets. /// The result from merging the child changesets together. /// Parameter was null. - public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, IEqualityComparer? equalityComparer = null) + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer) where TObject : notnull where TKey : notnull where TDestination : notnull @@ -3303,7 +3302,7 @@ public static IObservable> MergeManyCh { if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); - return source.MergeManyChangeSets((t, _) => observableSelector(t), comparer, equalityComparer); + return source.MergeManyChangeSets((t, _) => observableSelector(t), comparer); } /// @@ -3315,11 +3314,10 @@ public static IObservable> MergeManyCh /// The type of the destination key. /// The Source Observable ChangeSet. /// Factory Function used to create child changesets. - /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. - /// Optional instance to determine if two elements are the same. + /// instance to determine which element to emit if the same key is emitted from multiple child changesets. /// The result from merging the child changesets together. /// Parameter was null. - public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, IEqualityComparer? equalityComparer = null) + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer) where TObject : notnull where TKey : notnull where TDestination : notnull @@ -3329,7 +3327,7 @@ public static IObservable> MergeManyCh if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); if (comparer == null) throw new ArgumentNullException(nameof(comparer)); - return new MergeManyCacheChangeSetsSourceCompare(source, observableSelector, comparer, equalityComparer).Run(); + return source.MergeManyChangeSets(observableSelector, equalityComparer: null, comparer: comparer); } /// @@ -3341,19 +3339,19 @@ public static IObservable> MergeManyCh /// The type of the destination key. /// The Source Observable ChangeSet. /// Factory Function used to create child changesets. - /// instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. /// The result from merging the child changesets together. /// Parameter was null. - public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer) + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) where TObject : notnull where TKey : notnull where TDestination : notnull where TDestinationKey : notnull { - if (source == null) throw new ArgumentNullException(nameof(source)); if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); - return source.MergeManyChangeSets((t, _) => observableSelector(t), comparer); + return source.MergeManyChangeSets((t, _) => observableSelector(t), equalityComparer, comparer); } /// @@ -3365,10 +3363,11 @@ public static IObservable> MergeManyCh /// The type of the destination key. /// The Source Observable ChangeSet. /// Factory Function used to create child changesets. - /// instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. /// The result from merging the child changesets together. /// Parameter was null. - public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer) + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) where TObject : notnull where TKey : notnull where TDestination : notnull @@ -3376,13 +3375,38 @@ public static IObservable> MergeManyCh { if (source == null) throw new ArgumentNullException(nameof(source)); if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); - if (comparer == null) throw new ArgumentNullException(nameof(comparer)); - return source.MergeManyChangeSets(observableSelector, equalityComparer: null, comparer: comparer); + return new MergeManyCacheChangeSets(source, observableSelector, equalityComparer, comparer).Run(); } /// - /// Operator similiar to MergeMany except it is ChangeSet aware. It uses to transform each item in the source into a child and merges the result children together into a single stream of ChangeSets that correctly handles multiple Keys and removal of the parent items. + /// Overload of that + /// will handle key collisions by using an instance that operates on the sources, so that the values from the preferred source take precedent over other values with the same. + /// + /// The type of the object. + /// The type of the key. + /// The type of the destination. + /// The type of the destination key. + /// The Source Observable ChangeSet. + /// Factory Function used to create child changesets. + /// Optional instance to determine which parents items should be emitted when the same key is used by multiple child changesets. + /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. + /// The result from merging the child changesets together. + /// Parameter was null. + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, bool resortOnParentRefresh = false) + where TObject : notnull + where TKey : notnull + where TDestination : notnull + where TDestinationKey : notnull + { + if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); + + return source.MergeManyChangeSets((t, _) => observableSelector(t), comparer, resortOnParentRefresh); + } + + /// + /// Overload of that + /// will handle key collisions by using an instance that operates on the sources, so that the values from the preferred source take precedent over other values with the same. /// /// The type of the object. /// The type of the key. @@ -3390,23 +3414,26 @@ public static IObservable> MergeManyCh /// The type of the destination key. /// The Source Observable ChangeSet. /// Factory Function used to create child changesets. - /// Optional instance to determine if two elements are the same. /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. /// The result from merging the child changesets together. /// Parameter was null. - public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, bool resortOnParentRefresh = false) where TObject : notnull where TKey : notnull where TDestination : notnull where TDestinationKey : notnull { + if (source == null) throw new ArgumentNullException(nameof(source)); if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); + if (comparer == null) throw new ArgumentNullException(nameof(comparer)); - return source.MergeManyChangeSets((t, _) => observableSelector(t), equalityComparer, comparer); + return new MergeManyCacheChangeSetsSourceCompare(source, observableSelector, comparer, equalityComparer: null, resortOnParentRefresh).Run(); } /// - /// Operator similiar to MergeMany except it is ChangeSet aware. It uses to transform each item in the source into a child and merges the result children together into a single stream of ChangeSets that correctly handles multiple Keys and removal of the parent items. + /// Overload of that + /// will handle key collisions by using an instance that operates on the sources, so that the values from the preferred source take precedent over other values with the same. /// /// The type of the object. /// The type of the key. @@ -3414,11 +3441,38 @@ public static IObservable> MergeManyCh /// The type of the destination key. /// The Source Observable ChangeSet. /// Factory Function used to create child changesets. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. /// Optional instance to determine if two elements are the same. + /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. + /// The result from merging the child changesets together. + /// Parameter was null. + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, IEqualityComparer equalityComparer, bool resortOnParentRefresh = false) + where TObject : notnull + where TKey : notnull + where TDestination : notnull + where TDestinationKey : notnull + { + if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); + + return source.MergeManyChangeSets((t, _) => observableSelector(t), comparer, equalityComparer, resortOnParentRefresh); + } + + /// + /// Overload of that + /// will handle key collisions by using an instance that operates on the sources, so that the values from the preferred source take precedent over other values with the same. + /// + /// The type of the object. + /// The type of the key. + /// The type of the destination. + /// The type of the destination key. + /// The Source Observable ChangeSet. + /// Factory Function used to create child changesets. /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Optional instance to determine if two elements are the same. + /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. /// The result from merging the child changesets together. /// Parameter was null. - public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, IEqualityComparer equalityComparer, bool resortOnParentRefresh = false) where TObject : notnull where TKey : notnull where TDestination : notnull @@ -3426,8 +3480,10 @@ public static IObservable> MergeManyCh { if (source == null) throw new ArgumentNullException(nameof(source)); if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); + if (comparer == null) throw new ArgumentNullException(nameof(comparer)); + if (equalityComparer == null) throw new ArgumentNullException(nameof(equalityComparer)); - return new MergeManyCacheChangeSets(source, observableSelector, equalityComparer, comparer).Run(); + return new MergeManyCacheChangeSetsSourceCompare(source, observableSelector, comparer, equalityComparer, resortOnParentRefresh).Run(); } /// From ee0e983ee767492b386d9efb2463714d32e911ab Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Sat, 4 Nov 2023 15:47:52 -0700 Subject: [PATCH 3/8] More Progress --- .../Cache/MergeManyCacheChangeSetsFixture.cs | 18 - ...ManyCacheChangeSetsSourceCompareFixture.cs | 676 ++++++++++++++++++ src/DynamicData.Tests/Domain/Market.cs | 20 + .../{NoOps.cs => ComparerExtensions.cs} | 16 + .../MergeManyCacheChangeSetsSourceCompare.cs | 29 +- src/DynamicData/Cache/ObservableCacheEx.cs | 142 +++- 6 files changed, 856 insertions(+), 45 deletions(-) create mode 100644 src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs rename src/DynamicData.Tests/Utilities/{NoOps.cs => ComparerExtensions.cs} (54%) diff --git a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs index b13097968..21ba0650a 100644 --- a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs @@ -64,15 +64,6 @@ public void NullChecks() var actionChildCompare2a = () => nullChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: emptyChildComparer); var actionChildCompare2b = () => emptyChangeSetObs.MergeManyChangeSets(nullKeySelector, comparer: emptyChildComparer); var actionChildCompare2c = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: nullChildComparer); - var actionParentCompare1 = () => emptyChangeSetObs.MergeManyChangeSets(nullSelector, comparer: emptyParentComparer); - var actionParentCompareKey1a = () => nullChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: emptyParentComparer); - var actionParentCompareKey1b = () => emptyChangeSetObs.MergeManyChangeSets(nullKeySelector, comparer: emptyParentComparer); - var actionParentCompareKey1c = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: nullParentComparer); - var actionParentCompare2 = () => emptyChangeSetObs.MergeManyChangeSets(nullSelector, comparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); - var actionParentCompareKey2a = () => nullChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); - var actionParentCompareKey2b = () => emptyChangeSetObs.MergeManyChangeSets(nullKeySelector, comparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); - var actionParentCompareKey2c = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: nullParentComparer, equalityComparer: emptyEqualityComparer); - var actionParentCompareKey2d = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, comparer: emptyParentComparer, equalityComparer: nullEqualityComparer); // then emptyChangeSetObs.Should().NotBeNull(); @@ -96,15 +87,6 @@ public void NullChecks() actionChildCompare2a.Should().Throw(); actionChildCompare2b.Should().Throw(); actionChildCompare2c.Should().Throw(); - actionParentCompare1.Should().Throw(); - actionParentCompareKey1a.Should().Throw(); - actionParentCompareKey1b.Should().Throw(); - actionParentCompareKey1c.Should().Throw(); - actionParentCompare2.Should().Throw(); - actionParentCompareKey2a.Should().Throw(); - actionParentCompareKey2b.Should().Throw(); - actionParentCompareKey2c.Should().Throw(); - actionParentCompareKey2d.Should().Throw(); } [Fact] diff --git a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs new file mode 100644 index 000000000..a7d81b2fc --- /dev/null +++ b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs @@ -0,0 +1,676 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Linq; +using DynamicData.Kernel; +using DynamicData.Tests.Domain; +using DynamicData.Tests.Utilities; +using FluentAssertions; + +using Xunit; +using Xunit.Sdk; + +namespace DynamicData.Tests.Cache; + +public sealed class MergeManyCacheChangeSetsSourceCompareFixture : IDisposable +{ + const int MarketCount = 101; + const int PricesPerMarket = 103; + const int RemoveCount = 53; + const int ItemIdStride = 1000; + const decimal BasePrice = 10m; + const decimal PriceOffset = 10m; + const decimal HighestPrice = BasePrice + PriceOffset + 1.0m; + const decimal LowestPrice = BasePrice - 1.0m; + + private static readonly Random Random = new Random(0x21123737); + + private static decimal GetRandomPrice() => MarketPrice.RandomPrice(Random, BasePrice, PriceOffset); + + private readonly ISourceCache _marketCache = new SourceCache(p => p.Id); + + private readonly ChangeSetAggregator _marketCacheResults; + + public MergeManyCacheChangeSetsSourceCompareFixture() + { + _marketCacheResults = _marketCache.Connect().AsAggregator(); + } + + [Fact] + public void NullChecks() + { + // having + var emptyChangeSetObs = Observable.Empty>(); + var nullChangeSetObs = (IObservable>)null!; + var emptyChildChangeSetObs = Observable.Empty>(); + var emptySelector = new Func>>(i => emptyChildChangeSetObs); + var emptyKeySelector = new Func>>((i, key) => emptyChildChangeSetObs); + var nullSelector = (Func>>)null!; + var nullKeySelector = (Func>>)null!; + var nullParentComparer = (IComparer)null!; + var emptyParentComparer = new NoOpComparer() as IComparer; + var nullChildComparer = (IComparer)null!; + var emptyChildComparer = new NoOpComparer() as IComparer; + var nullEqualityComparer = (IEqualityComparer)null!; + var emptyEqualityComparer = new NoOpEqualityComparer() as IEqualityComparer; + + // when + var actionParentCompare1 = () => emptyChangeSetObs.MergeManyChangeSets(nullSelector, sourceComparer: emptyParentComparer); + var actionParentCompareKey1a = () => nullChangeSetObs.MergeManyChangeSets(emptyKeySelector, sourceComparer: emptyParentComparer); + var actionParentCompareKey1b = () => emptyChangeSetObs.MergeManyChangeSets(nullKeySelector, sourceComparer: emptyParentComparer); + var actionParentCompareKey1c = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, sourceComparer: nullParentComparer); + var actionParentCompare2 = () => emptyChangeSetObs.MergeManyChangeSets(nullSelector, sourceComparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); + var actionParentCompareKey2a = () => nullChangeSetObs.MergeManyChangeSets(emptyKeySelector, sourceComparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); + var actionParentCompareKey2b = () => emptyChangeSetObs.MergeManyChangeSets(nullKeySelector, sourceComparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); + var actionParentCompareKey2c = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, sourceComparer: nullParentComparer, equalityComparer: emptyEqualityComparer); + var actionParentCompareKey2d = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, sourceComparer: emptyParentComparer, equalityComparer: nullEqualityComparer); + + // then + emptyChangeSetObs.Should().NotBeNull(); + emptyChildChangeSetObs.Should().NotBeNull(); + emptyChildComparer.Should().NotBeNull(); + emptyEqualityComparer.Should().NotBeNull(); + emptyKeySelector.Should().NotBeNull(); + emptyParentComparer.Should().NotBeNull(); + emptySelector.Should().NotBeNull(); + nullChangeSetObs.Should().BeNull(); + nullChildComparer.Should().BeNull(); + nullEqualityComparer.Should().BeNull(); + nullKeySelector.Should().BeNull(); + nullParentComparer.Should().BeNull(); + nullSelector.Should().BeNull(); + + actionParentCompare1.Should().Throw(); + actionParentCompareKey1a.Should().Throw(); + actionParentCompareKey1b.Should().Throw(); + actionParentCompareKey1c.Should().Throw(); + actionParentCompare2.Should().Throw(); + actionParentCompareKey2a.Should().Throw(); + actionParentCompareKey2b.Should().Throw(); + actionParentCompareKey2c.Should().Throw(); + actionParentCompareKey2d.Should().Throw(); + } + + [Fact] + public void AbleToInvokeFactory() + { + // having + var invoked = false; + IObservable> factory(IMarket m) + { + invoked = true; + return m.LatestPrices; + } + using var sub = _marketCache.Connect().MergeManyChangeSets(factory, Market.RatingCompare).Subscribe(); + + // when + _marketCache.AddOrUpdate(new Market(0)); + + // then + _marketCacheResults.Data.Count.Should().Be(1); + invoked.Should().BeTrue(); + } + + [Fact] + public void AbleToInvokeFactoryWithKey() + { + // having + var invoked = false; + IObservable> factory(IMarket m, Guid g) + { + invoked = true; + return m.LatestPrices; + } + using var sub = _marketCache.Connect().MergeManyChangeSets(factory, Market.RatingCompare).Subscribe(); + + // when + _marketCache.AddOrUpdate(new Market(0)); + + // then + _marketCacheResults.Data.Count.Should().Be(1); + invoked.Should().BeTrue(); + } + +#if false + [Fact] + public void AllExistingSubItemsPresentInResult() + { + // having + var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + + // when + _marketCache.AddOrUpdate(markets); + + // then + _marketCacheResults.Data.Count.Should().Be(MarketCount); + markets.Sum(m => m.PricesCache.Count).Should().Be(MarketCount * PricesPerMarket); + results.Data.Count.Should().Be(MarketCount * PricesPerMarket); + results.Messages.Count.Should().Be(MarketCount); + results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + } + + [Fact] + public void AllNewSubItemsPresentInResult() + { + // having + var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + _marketCache.AddOrUpdate(markets); + + // when + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + + // then + _marketCacheResults.Data.Count.Should().Be(MarketCount); + markets.Sum(m => m.PricesCache.Count).Should().Be(MarketCount * PricesPerMarket); + results.Data.Count.Should().Be(MarketCount * PricesPerMarket); + results.Messages.Count.Should().Be(MarketCount); + results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + } + + [Fact] + public void AllRefreshedSubItemsAreRefreshed() + { + // having + var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + _marketCache.AddOrUpdate(markets); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + + // when + markets.ForEach(m => m.RefreshAllPrices(GetRandomPrice)); + + // then + _marketCacheResults.Data.Count.Should().Be(MarketCount); + results.Data.Count.Should().Be(MarketCount * PricesPerMarket); + results.Messages.Count.Should().Be(MarketCount * 2); + results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(MarketCount * PricesPerMarket); + } + + [Fact] + public void AnyDuplicateKeyValuesShouldBeHidden() + { + // having + var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + _marketCache.AddOrUpdate(markets); + + // when + markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Data.Items.Zip(markets[0].PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + } + + [Fact] + public void AnyDuplicateValuesShouldBeNoOpWhenRemoved() + { + // having + var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + _marketCache.AddOrUpdate(markets); + markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + + // when + markets[1].RemoveAllPrices(); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Data.Items.Zip(markets[0].PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + } + + [Fact] + public void AnyDuplicateValuesShouldBeUnhiddenWhenOtherIsRemoved() + { + // having + var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + _marketCache.AddOrUpdate(markets); + markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + + // when + _marketCache.Remove(markets[0]); + + // then + _marketCacheResults.Data.Count.Should().Be(1); + results.Data.Count.Should().Be(PricesPerMarket); + results.Data.Items.Zip(markets[1].PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); + results.Messages.Count.Should().Be(2); + results.Messages[1].Updates.Should().Be(PricesPerMarket); + } + + [Fact] + public void AnyDuplicateValuesShouldNotRefreshWhenHidden() + { + // having + var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + _marketCache.AddOrUpdate(markets); + markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + + // when + markets[1].RefreshAllPrices(GetRandomPrice); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Summary.Overall.Refreshes.Should().Be(0); + results.Data.Items.Zip(markets[0].PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); + } + + [Fact] + public void AnyRemovedSubItemIsRemoved() + { + // having + var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + _marketCache.AddOrUpdate(markets); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + + // when + markets.ForEach(m => m.PricesCache.Edit(updater => updater.RemoveKeys(updater.Keys.Take(RemoveCount)))); + + // then + _marketCacheResults.Data.Count.Should().Be(MarketCount); + results.Data.Count.Should().Be(MarketCount * (PricesPerMarket - RemoveCount)); + results.Messages.Count.Should().Be(MarketCount * 2); + results.Messages[0].Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(MarketCount * RemoveCount); + } + + [Fact] + public void AnySourceItemRemovedRemovesAllSourceValues() + { + // having + var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + _marketCache.AddOrUpdate(markets); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + + // when + _marketCache.Edit(updater => updater.RemoveKeys(updater.Keys.Take(RemoveCount))); + + // then + _marketCacheResults.Data.Count.Should().Be(MarketCount - RemoveCount); + results.Data.Count.Should().Be((MarketCount - RemoveCount) * PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(PricesPerMarket * RemoveCount); + } + + [Fact] + public void ChangingSourceByUpdateRemovesPreviousAndAddsNewValues() + { + // having + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + var market = new Market(0); + market.AddRandomPrices(0, PricesPerMarket * 2, GetRandomPrice); + _marketCache.AddOrUpdate(market); + var updatedMarket = new Market(market); + updatedMarket.AddRandomPrices(PricesPerMarket, PricesPerMarket * 3, GetRandomPrice); + + // when + _marketCache.AddOrUpdate(updatedMarket); + + // then + _marketCacheResults.Data.Count.Should().Be(1); + results.Data.Count.Should().Be(PricesPerMarket * 2); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket * 3); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(PricesPerMarket); + results.Data.Items.Zip(updatedMarket.PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); + } +#endif + + [Fact] + public void ComparerOnlyAddsBetterAddedValues() + { + // having + using var results = CreateRatingOnlyResults(false); + var marketOriginal = new Market(0); + var marketBetter = new Market(1); + marketBetter.Rating = 1.0; + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketCache.AddOrUpdate(marketOriginal); + + // when + _marketCache.AddOrUpdate(marketBetter); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + } + + [Fact] + public void ComparerOnlyAddsBetterExistingValues() + { + // having + var marketOriginal = new Market(0); + var marketBetter = new Market(1); + marketBetter.Rating = 1.0; + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketCache.AddOrUpdate(marketOriginal); + _marketCache.AddOrUpdate(marketBetter); + + // when + using var results = CreateRatingOnlyResults(false); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Updates.Should().Be(0); + results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + } + + [Fact] + public void ComparerOnlyAddsBetterValuesOnSourceUpdate() + { + // having + using var results = CreateRatingOnlyResults(false); + var marketOriginal = new Market(0); + var marketBetter = new Market(1); + marketBetter.Rating = 1.0; + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketCache.AddOrUpdate(marketOriginal); + _marketCache.AddOrUpdate(marketBetter); + + // when + marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + } + + [Fact] + public void ComparerUpdatesToCorrectValueOnRefresh() + { + // having + using var results = CreateRatingOnlyResults(true); + var marketOriginal = new Market(0); + var marketBetter = new Market(1); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketCache.AddOrUpdate(marketOriginal); + _marketCache.AddOrUpdate(marketBetter); + + // when + SetRating(marketBetter, 1.0); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + } + + [Fact] + public void ComparerUpdatesToCorrectValueOnRemove() + { + // having + var marketOriginal = new Market(0); + var marketBetter = new Market(1); + var marketBest = new Market(2); + marketBetter.Rating = 1.0; + marketBest.Rating = 5.0; + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketBest.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketCache.AddOrUpdate(marketOriginal); + _marketCache.AddOrUpdate(marketBest); + _marketCache.AddOrUpdate(marketBetter); + using var results = CreateRatingOnlyResults(false); + + // when + _marketCache.Remove(marketBest); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + } + + [Fact] + public void ComparerUpdatesToCorrectValueOnUpdate() + { + // having + using var results = CreateRatingOnlyResults(false); + using var resultsLow = CreateLowRatingResults(false); + var marketOriginal = new Market(0); + var marketBetter = new Market(1); + marketBetter.Rating = 1.0; + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketCache.AddOrUpdate(marketOriginal); + _marketCache.AddOrUpdate(marketBetter); + + // when + marketBetter.UpdateAllPrices(GetRandomPrice()); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); + results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + resultsLow.Data.Count.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); + } + +#if false + [Fact] + public void ComparerOnlyUpdatesVisibleValuesOnUpdate() + { + // having + using var highPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.HighPriceCompare).AsAggregator(); + using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); + var marketOriginal = new Market(0); + var marketLow = new Market(1); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); + _marketCache.AddOrUpdate(marketOriginal); + _marketCache.AddOrUpdate(marketLow); + + // when + marketLow.UpdateAllPrices(LowestPrice - 1); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + lowPriceResults.Data.Count.Should().Be(PricesPerMarket); + lowPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); + lowPriceResults.Summary.Overall.Removes.Should().Be(0); + lowPriceResults.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); + lowPriceResults.Summary.Overall.Refreshes.Should().Be(0); + lowPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketLow.Id)); + highPriceResults.Data.Count.Should().Be(PricesPerMarket); + highPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); + highPriceResults.Summary.Overall.Removes.Should().Be(0); + highPriceResults.Summary.Overall.Updates.Should().Be(0); + highPriceResults.Summary.Overall.Refreshes.Should().Be(0); + highPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); + } + + [Fact] + public void ComparerOnlyRefreshesVisibleValues() + { + // having + using var highPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer, MarketPrice.HighPriceCompare).AsAggregator(); + using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer, MarketPrice.LowPriceCompare).AsAggregator(); + var marketOriginal = new Market(0); + var marketLow = new Market(1); + marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); + _marketCache.AddOrUpdate(marketOriginal); + _marketCache.AddOrUpdate(marketLow); + + // when + marketLow.RefreshAllPrices(LowestPrice - 1); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + lowPriceResults.Data.Count.Should().Be(PricesPerMarket); + lowPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); + lowPriceResults.Summary.Overall.Removes.Should().Be(0); + lowPriceResults.Summary.Overall.Updates.Should().Be(PricesPerMarket); + lowPriceResults.Summary.Overall.Refreshes.Should().Be(PricesPerMarket); + lowPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketLow.Id)); + highPriceResults.Data.Count.Should().Be(PricesPerMarket); + highPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); + highPriceResults.Summary.Overall.Removes.Should().Be(0); + highPriceResults.Summary.Overall.Updates.Should().Be(0); + highPriceResults.Summary.Overall.Refreshes.Should().Be(0); + highPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); + } + + [Fact] + public void EqualityComparerHidesUpdatesWithoutChanges() + { + // having + var market = new Market(0); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + market.UpdatePrices(0, PricesPerMarket, LowestPrice); + _marketCache.AddOrUpdate(market); + + // when + market.UpdatePrices(0, PricesPerMarket, LowestPrice); + + // then + _marketCacheResults.Data.Count.Should().Be(1); + results.Data.Count.Should().Be(PricesPerMarket); + results.Messages.Count.Should().Be(1); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); + } +#endif + + [Fact] + public void EveryItemVisibleWhenSequenceCompletes() + { + // having + _marketCache.AddOrUpdate(Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(GetRandomPrice, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket))); + + // when + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare).AsAggregator(); + DisposeMarkets(); + + // then + results.Data.Count.Should().Be(PricesPerMarket * MarketCount); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket * MarketCount); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void MergedObservableCompletesOnlyWhenSourceAndAllChildrenComplete(bool completeSource, bool completeChildren) + { + // having + _marketCache.AddOrUpdate(Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(GetRandomPrice, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket, completable: completeChildren))); + var hasSourceSequenceCompleted = false; + var hasMergedSequenceCompleted = false; + + using var cleanup = _marketCache.Connect().Do(_ => { }, () => hasSourceSequenceCompleted = true) + .MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare).Subscribe(_ => { }, () => hasMergedSequenceCompleted = true); + + // when + if (completeSource) + { + DisposeMarkets(); + } + + // then + hasSourceSequenceCompleted.Should().Be(completeSource); + hasMergedSequenceCompleted.Should().Be(completeSource && completeChildren); + } + + [Fact] + public void MergedObservableWillFailIfSourceFails() + { + // having + var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); + _marketCache.AddOrUpdate(markets); + var receivedError = default(Exception); + var expectedError = new Exception("Test exception"); + var throwObservable = Observable.Throw>(expectedError); + + using var cleanup = _marketCache.Connect().Concat(throwObservable) + .MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare).Subscribe(_ => { }, err => receivedError = err); + + // when + DisposeMarkets(); + + // then + receivedError.Should().Be(expectedError); + } + + private ChangeSetAggregator CreateRatingOnlyResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh).AsAggregator(); + private ChangeSetAggregator CreateLowRatingResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare.Invert(), resortOnSourceRefresh: resortOnRefresh).AsAggregator(); + private ChangeSetAggregator CreateRatingThenHighResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.HighPriceCompare).AsAggregator(); + private ChangeSetAggregator CreateRatingThenLowResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.LowPriceCompare).AsAggregator(); + private ChangeSetAggregator CreateRatingThenRecentResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); + private ChangeSetAggregator CreateRatingThenRecentNoTimestampResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); + + private IMarket SetRating(IMarket market, double newRating) + { + market.Rating = newRating; + _marketCache.Refresh(market); + return market; + } + + public void Dispose() + { + _marketCacheResults.Dispose(); + DisposeMarkets(); + } + + private void DisposeMarkets() + { + _marketCache.Items.ForEach(m => (m as IDisposable)?.Dispose()); + _marketCache.Dispose(); + _marketCache.Clear(); + } +} diff --git a/src/DynamicData.Tests/Domain/Market.cs b/src/DynamicData.Tests/Domain/Market.cs index b13b33a33..b546205ab 100644 --- a/src/DynamicData.Tests/Domain/Market.cs +++ b/src/DynamicData.Tests/Domain/Market.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; using System.Linq; using System.Reactive.Linq; using DynamicData.Kernel; @@ -10,6 +13,8 @@ internal interface IMarket { public string Name { get; } + public double Rating { get; set; } + public Guid Id { get; } public IObservable> LatestPrices { get; } @@ -19,6 +24,8 @@ internal class Market : IMarket, IDisposable { private readonly ISourceCache _latestPrices = new SourceCache(p => p.ItemId); + public static IComparer RatingCompare { get; } = new RatingComparer(); + private Market(string name, Guid id) { Name = name; @@ -37,6 +44,8 @@ public Market(int name) : this($"Market #{name}", Guid.NewGuid()) public Guid Id { get; } + public double Rating { get; set; } + public IObservable> LatestPrices => _latestPrices.Connect(); public ISourceCache PricesCache => _latestPrices; @@ -89,6 +98,15 @@ public Market RefreshPrice(int id, decimal newPrice) public Market UpdatePrices(int minId, int maxId, decimal newPrice) => this.With(_ => _latestPrices.AddOrUpdate(Enumerable.Range(minId, maxId - minId).Select(id => CreatePrice(id, newPrice)))); public void Dispose() => _latestPrices.Dispose(); + + private class RatingComparer : IComparer + { + public int Compare([DisallowNull] IMarket x, [DisallowNull] IMarket y) + { + // Higher ratings go first + return y.Rating.CompareTo(x.Rating); + } + } } @@ -106,5 +124,7 @@ public FixedMarket(Func getPrice, int minId, int maxId, bool completabl public string Name => Id.ToString("B"); + public double Rating { get; set; } + public Guid Id { get; } } diff --git a/src/DynamicData.Tests/Utilities/NoOps.cs b/src/DynamicData.Tests/Utilities/ComparerExtensions.cs similarity index 54% rename from src/DynamicData.Tests/Utilities/NoOps.cs rename to src/DynamicData.Tests/Utilities/ComparerExtensions.cs index f57025517..1908f4cf8 100644 --- a/src/DynamicData.Tests/Utilities/NoOps.cs +++ b/src/DynamicData.Tests/Utilities/ComparerExtensions.cs @@ -14,3 +14,19 @@ internal class NoOpEqualityComparer : IEqualityComparer public bool Equals(T x, T y) => throw new NotImplementedException(); public int GetHashCode([DisallowNull] T obj) => throw new NotImplementedException(); } + + +internal class InvertedComparer : IComparer +{ + private readonly IComparer _original; + + public InvertedComparer(IComparer original) => _original = original; + + public int Compare(T x, T y) => _original.Compare(x, y) * -1; +} + + +internal static class ComparerExtensions +{ + public static IComparer Invert(this IComparer comparer) => new InvertedComparer(comparer); +} diff --git a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs index 40dd2fba1..69b7d7361 100644 --- a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs +++ b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs @@ -27,11 +27,11 @@ internal sealed class MergeManyCacheChangeSetsSourceCompare> source, Func>> selector, IComparer comparer, IEqualityComparer? equalityComparer, bool reevalOnRefresh = false) + public MergeManyCacheChangeSetsSourceCompare(IObservable> source, Func>> selector, IComparer parentCompare, IEqualityComparer? equalityComparer, IComparer? childCompare, bool reevalOnRefresh = false) { _source = source; _changeSetSelector = (obj, key) => selector(obj, key).Transform(dest => new ParentChildEntry(obj, dest)); - _comparer = new ParentChildCompare(comparer); + _comparer = (childCompare is null) ? new ParentOnlyCompare(parentCompare) : new ParentChildCompare(parentCompare, childCompare); _equalityComparer = (equalityComparer != null) ? new ParentChildEqualityCompare(equalityComparer) : null; _reevalOnRefresh = reevalOnRefresh; } @@ -93,11 +93,30 @@ public ParentChildEntry(TObject parent, TDestination child) private class ParentChildCompare : IComparer { - private readonly IComparer _comparer; + private readonly IComparer _comparerParent; + private readonly IComparer _comparerChild; - public ParentChildCompare(IComparer comparer) => _comparer = comparer; + public ParentChildCompare(IComparer comparerParent, IComparer comparerChild) + { + _comparerParent = comparerParent; + _comparerChild = comparerChild; + } + + public int Compare(ParentChildEntry x, ParentChildEntry y) => + _comparerParent.Compare(x.Parent, y.Parent) switch + { + 0 => _comparerChild.Compare(x.Child, x.Child), + int i => i, + }; + } + + private class ParentOnlyCompare : IComparer + { + private readonly IComparer _comparerParent; + + public ParentOnlyCompare(IComparer comparer) => _comparerParent = comparer; - public int Compare(ParentChildEntry x, ParentChildEntry y) => _comparer.Compare(x.Parent, y.Parent); + public int Compare(ParentChildEntry x, ParentChildEntry y) => _comparerParent.Compare(x.Parent, y.Parent); } private class ParentChildEqualityCompare : IEqualityComparer diff --git a/src/DynamicData/Cache/ObservableCacheEx.cs b/src/DynamicData/Cache/ObservableCacheEx.cs index cfeac60c6..8990c8f4b 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.cs @@ -27,6 +27,7 @@ namespace DynamicData; public static class ObservableCacheEx { private const int DefaultSortResetThreshold = 100; + private const bool DefaultResortOnSourceRefresh = false; /// /// Inject side effects into the stream using the specified adaptor. @@ -3380,7 +3381,7 @@ public static IObservable> MergeManyCh } /// - /// Overload of that + /// Overload of that /// will handle key collisions by using an instance that operates on the sources, so that the values from the preferred source take precedent over other values with the same. /// /// The type of the object. @@ -3389,11 +3390,11 @@ public static IObservable> MergeManyCh /// The type of the destination key. /// The Source Observable ChangeSet. /// Factory Function used to create child changesets. - /// Optional instance to determine which parents items should be emitted when the same key is used by multiple child changesets. - /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. + /// instance to determine which source elements child to use when two sources provide a child element with the same key. + /// Optional fallback instance to determine which child element to emit if the sources compare to be the same. /// The result from merging the child changesets together. /// Parameter was null. - public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, bool resortOnParentRefresh = false) + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer sourceComparer, IComparer childComparer) where TObject : notnull where TKey : notnull where TDestination : notnull @@ -3401,11 +3402,11 @@ public static IObservable> MergeManyCh { if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); - return source.MergeManyChangeSets((t, _) => observableSelector(t), comparer, resortOnParentRefresh); + return source.MergeManyChangeSets((t, _) => observableSelector(t), sourceComparer, DefaultResortOnSourceRefresh, equalityComparer: null, childComparer); } /// - /// Overload of that + /// Overload of that /// will handle key collisions by using an instance that operates on the sources, so that the values from the preferred source take precedent over other values with the same. /// /// The type of the object. @@ -3414,21 +3415,67 @@ public static IObservable> MergeManyCh /// The type of the destination key. /// The Source Observable ChangeSet. /// Factory Function used to create child changesets. - /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. - /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. + /// instance to determine which source elements child to use when two sources provide a child element with the same key. + /// Optional fallback instance to determine which child element to emit if the sources compare to be the same. /// The result from merging the child changesets together. /// Parameter was null. - public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, bool resortOnParentRefresh = false) + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer sourceComparer, IComparer childComparer) + where TObject : notnull + where TKey : notnull + where TDestination : notnull + where TDestinationKey : notnull + { + return source.MergeManyChangeSets(observableSelector, sourceComparer, DefaultResortOnSourceRefresh, equalityComparer: null, childComparer); + } + + /// + /// Overload of that + /// will handle key collisions by using an instance that operates on the sources, so that the values from the preferred source take precedent over other values with the same. + /// + /// The type of the object. + /// The type of the key. + /// The type of the destination. + /// The type of the destination key. + /// The Source Observable ChangeSet. + /// Factory Function used to create child changesets. + /// instance to determine which source elements child to use when two sources provide a child element with the same key. + /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. + /// Optional fallback instance to determine which child element to emit if the sources compare to be the same. + /// The result from merging the child changesets together. + /// Parameter was null. + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer sourceComparer, bool resortOnSourceRefresh, IComparer childComparer) where TObject : notnull where TKey : notnull where TDestination : notnull where TDestinationKey : notnull { - if (source == null) throw new ArgumentNullException(nameof(source)); if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); - if (comparer == null) throw new ArgumentNullException(nameof(comparer)); - return new MergeManyCacheChangeSetsSourceCompare(source, observableSelector, comparer, equalityComparer: null, resortOnParentRefresh).Run(); + return source.MergeManyChangeSets((t, _) => observableSelector(t), sourceComparer, resortOnSourceRefresh, equalityComparer: null, childComparer); + } + + /// + /// Overload of that + /// will handle key collisions by using an instance that operates on the sources, so that the values from the preferred source take precedent over other values with the same. + /// + /// The type of the object. + /// The type of the key. + /// The type of the destination. + /// The type of the destination key. + /// The Source Observable ChangeSet. + /// Factory Function used to create child changesets. + /// instance to determine which source elements child to use when two sources provide a child element with the same key. + /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. + /// Optional fallback instance to determine which child element to emit if the sources compare to be the same. + /// The result from merging the child changesets together. + /// Parameter was null. + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer sourceComparer, bool resortOnSourceRefresh, IComparer childComparer) + where TObject : notnull + where TKey : notnull + where TDestination : notnull + where TDestinationKey : notnull + { + return source.MergeManyChangeSets(observableSelector, sourceComparer, resortOnSourceRefresh, equalityComparer: null, childComparer); } /// @@ -3441,12 +3488,12 @@ public static IObservable> MergeManyCh /// The type of the destination key. /// The Source Observable ChangeSet. /// Factory Function used to create child changesets. - /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// instance to determine which source elements child to use when two sources provide a child element with the same key. /// Optional instance to determine if two elements are the same. - /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. + /// Optional fallback instance to determine which child element to emit if the sources compare to be the same. /// The result from merging the child changesets together. /// Parameter was null. - public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, IEqualityComparer equalityComparer, bool resortOnParentRefresh = false) + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer sourceComparer, IEqualityComparer? equalityComparer = null, IComparer? childComparer = null) where TObject : notnull where TKey : notnull where TDestination : notnull @@ -3454,7 +3501,7 @@ public static IObservable> MergeManyCh { if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); - return source.MergeManyChangeSets((t, _) => observableSelector(t), comparer, equalityComparer, resortOnParentRefresh); + return source.MergeManyChangeSets((t, _) => observableSelector(t), sourceComparer, DefaultResortOnSourceRefresh, equalityComparer, childComparer); } /// @@ -3467,12 +3514,64 @@ public static IObservable> MergeManyCh /// The type of the destination key. /// The Source Observable ChangeSet. /// Factory Function used to create child changesets. - /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// instance to determine which source elements child to use when two sources provide a child element with the same key. /// Optional instance to determine if two elements are the same. - /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. + /// Optional fallback instance to determine which child element to emit if the sources compare to be the same. /// The result from merging the child changesets together. /// Parameter was null. - public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer comparer, IEqualityComparer equalityComparer, bool resortOnParentRefresh = false) + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer sourceComparer, IEqualityComparer? equalityComparer = null, IComparer? childComparer = null) + where TObject : notnull + where TKey : notnull + where TDestination : notnull + where TDestinationKey : notnull + { + return source.MergeManyChangeSets(observableSelector, sourceComparer, DefaultResortOnSourceRefresh, equalityComparer, childComparer); + } + + /// + /// Overload of that + /// will handle key collisions by using an instance that operates on the sources, so that the values from the preferred source take precedent over other values with the same. + /// + /// The type of the object. + /// The type of the key. + /// The type of the destination. + /// The type of the destination key. + /// The Source Observable ChangeSet. + /// Factory Function used to create child changesets. + /// instance to determine which source elements child to use when two sources provide a child element with the same key. + /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. + /// Optional instance to determine if two elements are the same. + /// Optional fallback instance to determine which child element to emit if the sources compare to be the same. + /// The result from merging the child changesets together. + /// Parameter was null. + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer sourceComparer, bool resortOnSourceRefresh, IEqualityComparer? equalityComparer = null, IComparer? childComparer = null) + where TObject : notnull + where TKey : notnull + where TDestination : notnull + where TDestinationKey : notnull + { + if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); + + return source.MergeManyChangeSets((t, _) => observableSelector(t), sourceComparer, resortOnSourceRefresh, equalityComparer, childComparer); + } + + /// + /// Overload of that + /// will handle key collisions by using an instance that operates on the sources, so that the values from the preferred source take precedent over other values with the same. + /// + /// The type of the object. + /// The type of the key. + /// The type of the destination. + /// The type of the destination key. + /// The Source Observable ChangeSet. + /// Factory Function used to create child changesets. + /// instance to determine which source elements child to use when two sources provide a child element with the same key. + /// Optional boolean to indicate whether or not a refresh event in the parent stream should re-evaluate item priorities. + /// Optional instance to determine if two elements are the same. + /// Optional fallback instance to determine which child element to emit if the sources compare to be the same. + /// The result from merging the child changesets together. + /// Parameter was null. + public static IObservable> MergeManyChangeSets(this IObservable> source, Func>> observableSelector, IComparer sourceComparer, bool resortOnSourceRefresh, IEqualityComparer? equalityComparer = null, IComparer? childComparer = null) where TObject : notnull where TKey : notnull where TDestination : notnull @@ -3480,10 +3579,9 @@ public static IObservable> MergeManyCh { if (source == null) throw new ArgumentNullException(nameof(source)); if (observableSelector == null) throw new ArgumentNullException(nameof(observableSelector)); - if (comparer == null) throw new ArgumentNullException(nameof(comparer)); - if (equalityComparer == null) throw new ArgumentNullException(nameof(equalityComparer)); + if (sourceComparer == null) throw new ArgumentNullException(nameof(sourceComparer)); - return new MergeManyCacheChangeSetsSourceCompare(source, observableSelector, comparer, equalityComparer, resortOnParentRefresh).Run(); + return new MergeManyCacheChangeSetsSourceCompare(source, observableSelector, sourceComparer, equalityComparer, childComparer, resortOnSourceRefresh).Run(); } /// From e3b042ee5518d85ed27a695eb179dcd60291791c Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Sat, 4 Nov 2023 15:48:02 -0700 Subject: [PATCH 4/8] Revert Refresh improvement --- .../Cache/Internal/ChangeSetMergeTracker.cs | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs index 244e3a7c0..5b60953a4 100644 --- a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs +++ b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs @@ -185,33 +185,19 @@ private void OnItemRefreshed(ChangeSetCache[] sources, TObject it { var cached = _resultCache.Lookup(key); - // Only proceed if the key has a current value - if (cached.HasValue) + // Received a refresh change for a key that hasn't been seen yet + // Nothing can be done, so ignore it + if (!cached.HasValue) { - // If the refreshed value is the current one - if (ReferenceEquals(cached.Value, item)) - { - // When using a compare and the current value has changed, so do a full search for - // the best value to make sure the current choice is still the best choice - if ((_comparer is not null) && UpdateToBestValue(sources, key, cached)) - { - // A new value was choosen, so there's nothing left to do - return; - } + return; + } - // The current one is still the best choice and it was refreshed, so - // emit the Refresh downstream so consumers will see it. - _resultCache.Refresh(key); - } - else - { - // If the current value isn't being refreshed and using a comparer, - // check if the refreshed item is now a better choice - if ((_comparer is not null) && ShouldReplace(item, cached.Value)) - { - _resultCache.AddOrUpdate(item, key); - } - } + // In the sorting case, a refresh requires doing a full update because any change could alter what the best value is + // If we don't care about sorting OR if we do care, but re-selecting the best value didn't change anything + // AND the current value is the exact one being refreshed, then emit the refresh downstream + if (((_comparer is null) || !UpdateToBestValue(sources, key, cached)) && ReferenceEquals(cached.Value, item)) + { + _resultCache.Refresh(key); } } From 46feed2b6ed339b4a76024a62d8ee0a88ffb6b0d Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Sat, 4 Nov 2023 17:12:12 -0700 Subject: [PATCH 5/8] More progress... Needs a few more tests --- ...ManyCacheChangeSetsSourceCompareFixture.cs | 42 +++++++++++-------- .../Cache/Internal/ChangeSetMergeTracker.cs | 2 +- .../MergeManyCacheChangeSetsSourceCompare.cs | 29 +++++++++---- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs index a7d81b2fc..14ddb7ea1 100644 --- a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs @@ -16,8 +16,10 @@ namespace DynamicData.Tests.Cache; public sealed class MergeManyCacheChangeSetsSourceCompareFixture : IDisposable { - const int MarketCount = 101; - const int PricesPerMarket = 103; + const int MarketCount = 5; + const int PricesPerMarket = 7; + //const int MarketCount = 101; + //const int PricesPerMarket = 103; const int RemoveCount = 53; const int ItemIdStride = 1000; const decimal BasePrice = 10m; @@ -350,7 +352,7 @@ public void ChangingSourceByUpdateRemovesPreviousAndAddsNewValues() public void ComparerOnlyAddsBetterAddedValues() { // having - using var results = CreateRatingOnlyResults(false); + using var results = ChangeSetByRatingOnly(false).AsAggregator(); var marketOriginal = new Market(0); var marketBetter = new Market(1); marketBetter.Rating = 1.0; @@ -382,13 +384,13 @@ public void ComparerOnlyAddsBetterExistingValues() _marketCache.AddOrUpdate(marketBetter); // when - using var results = CreateRatingOnlyResults(false); + using var results = ChangeSetByRatingOnly(false).DebugSpy("Rating").AsAggregator(); // then _marketCacheResults.Data.Count.Should().Be(2); results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); - results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); } @@ -396,7 +398,7 @@ public void ComparerOnlyAddsBetterExistingValues() public void ComparerOnlyAddsBetterValuesOnSourceUpdate() { // having - using var results = CreateRatingOnlyResults(false); + using var results = ChangeSetByRatingOnly(false).AsAggregator(); var marketOriginal = new Market(0); var marketBetter = new Market(1); marketBetter.Rating = 1.0; @@ -419,7 +421,7 @@ public void ComparerOnlyAddsBetterValuesOnSourceUpdate() public void ComparerUpdatesToCorrectValueOnRefresh() { // having - using var results = CreateRatingOnlyResults(true); + using var results = ChangeSetByRatingOnly(true).AsAggregator(); var marketOriginal = new Market(0); var marketBetter = new Market(1); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); @@ -453,7 +455,7 @@ public void ComparerUpdatesToCorrectValueOnRemove() _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketBest); _marketCache.AddOrUpdate(marketBetter); - using var results = CreateRatingOnlyResults(false); + using var results = ChangeSetByRatingOnly(false).DebugSpy("Results").AsAggregator(); // when _marketCache.Remove(marketBest); @@ -462,7 +464,7 @@ public void ComparerUpdatesToCorrectValueOnRemove() _marketCacheResults.Data.Count.Should().Be(2); results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); - results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); } @@ -470,8 +472,8 @@ public void ComparerUpdatesToCorrectValueOnRemove() public void ComparerUpdatesToCorrectValueOnUpdate() { // having - using var results = CreateRatingOnlyResults(false); - using var resultsLow = CreateLowRatingResults(false); + using var results = ChangeSetByRatingOnly(false).DebugSpy("Rating").AsAggregator(); + using var resultsLow = ChangeSetByLowRating(false).DebugSpy("Rating Low").AsAggregator(); var marketOriginal = new Market(0); var marketBetter = new Market(1); marketBetter.Rating = 1.0; @@ -647,12 +649,18 @@ public void MergedObservableWillFailIfSourceFails() receivedError.Should().Be(expectedError); } - private ChangeSetAggregator CreateRatingOnlyResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh).AsAggregator(); - private ChangeSetAggregator CreateLowRatingResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare.Invert(), resortOnSourceRefresh: resortOnRefresh).AsAggregator(); - private ChangeSetAggregator CreateRatingThenHighResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.HighPriceCompare).AsAggregator(); - private ChangeSetAggregator CreateRatingThenLowResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.LowPriceCompare).AsAggregator(); - private ChangeSetAggregator CreateRatingThenRecentResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); - private ChangeSetAggregator CreateRatingThenRecentNoTimestampResults(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); + private IObservable> CreateChangeSet(string name, IComparer? sourceComp = null, IComparer? childCompare = null, IEqualityComparer? equalityComparer = null, bool resortOnRefresh = true) => + _marketCache.Connect() + .DebugSpy(name) + .MergeManyChangeSets(m => m.LatestPrices.DebugSpy($"{name} [{m.Name} Prices]"), sourceComp ?? Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, equalityComparer, childCompare) + .DebugSpy($"{name} [Results]"); + + private IObservable> ChangeSetByRatingOnly(bool resortOnRefresh = true) => CreateChangeSet("Rating", resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByLowRating(bool resortOnRefresh = true) => CreateChangeSet("Rating Low", Market.RatingCompare.Invert(), resortOnRefresh: resortOnRefresh); + //private IObservable> ChangeSetByRatingThenHigh(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices.DebugSpy($"Rating / High [{m.Name}]"), Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.HighPriceCompare); + //private IObservable> ChangeSetByRatingThenLow(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices.DebugSpy($"Rating / Low [{m.Name}]"), Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.LowPriceCompare); + //private IObservable> ChangeSetByRatingThenRecent(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices.DebugSpy($"Rating / Recent [{m.Id}]"), Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare); + //private IObservable> ChangeSetByRatingThenRecentNoTimestamp(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices.DebugSpy($"Rating / Recent (No TS) [{m.Id}]"), Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare); private IMarket SetRating(IMarket market, double newRating) { diff --git a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs index 5b60953a4..e5b7aeaba 100644 --- a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs +++ b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs @@ -262,7 +262,7 @@ private Optional LookupBestValue(ChangeSetCache[] source } private bool CheckEquality(TObject left, TObject right) => - ReferenceEquals(left, right) || (_equalityComparer?.Equals(left, right) ?? (_comparer?.Compare(left, right) == 0)); + ReferenceEquals(left, right) || (_equalityComparer?.Equals(left, right) ?? false); // Return true if candidate should replace current as the observed downstream value private bool ShouldReplace(TObject candidate, TObject current) => diff --git a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs index 69b7d7361..d9631ffed 100644 --- a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs +++ b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs @@ -2,6 +2,7 @@ // Roland Pheasant licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -78,7 +79,7 @@ public IObservable> Run() }).Transform(entry => entry.Child); } - private readonly struct ParentChildEntry + private class ParentChildEntry { public ParentChildEntry(TObject parent, TDestination child) { @@ -102,12 +103,15 @@ public ParentChildCompare(IComparer comparerParent, IComparer - _comparerParent.Compare(x.Parent, y.Parent) switch - { - 0 => _comparerChild.Compare(x.Child, x.Child), - int i => i, - }; + public int Compare(ParentChildEntry? x, ParentChildEntry? y) => + (x is null && y is null) ? 0 + : (x is null) ? 1 + : (y is null) ? -1 + : _comparerParent.Compare(x.Parent, y.Parent) switch + { + 0 => _comparerChild.Compare(x.Child, x.Child), + int i => i, + }; } private class ParentOnlyCompare : IComparer @@ -116,7 +120,11 @@ private class ParentOnlyCompare : IComparer public ParentOnlyCompare(IComparer comparer) => _comparerParent = comparer; - public int Compare(ParentChildEntry x, ParentChildEntry y) => _comparerParent.Compare(x.Parent, y.Parent); + public int Compare(ParentChildEntry? x, ParentChildEntry? y) => + (x is null && y is null) ? 0 + : (x is null) ? 1 + : (y is null) ? -1 + : _comparerParent.Compare(x.Parent, y.Parent); } private class ParentChildEqualityCompare : IEqualityComparer @@ -125,7 +133,10 @@ private class ParentChildEqualityCompare : IEqualityComparer public ParentChildEqualityCompare(IEqualityComparer comparer) => _comparer = comparer; - public bool Equals(ParentChildEntry x, ParentChildEntry y) => _comparer.Equals(x.Child, y.Child); + public bool Equals(ParentChildEntry? x, ParentChildEntry? y) => + (x is null && y is null) ? true + : (x is null || y is null) ? false + : _comparer.Equals(x.Child, y.Child); public int GetHashCode(ParentChildEntry obj) => _comparer.GetHashCode(obj.Child); } From 3b40df209900452ba93730bad7c8ddeeb70b22d7 Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Sun, 5 Nov 2023 13:24:23 -0800 Subject: [PATCH 6/8] More Unit Tests --- ...ManyCacheChangeSetsSourceCompareFixture.cs | 243 +++++++++++++----- .../MergeManyCacheChangeSetsSourceCompare.cs | 52 ++-- 2 files changed, 210 insertions(+), 85 deletions(-) diff --git a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs index 14ddb7ea1..a11e2afc5 100644 --- a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs @@ -18,9 +18,10 @@ public sealed class MergeManyCacheChangeSetsSourceCompareFixture : IDisposable { const int MarketCount = 5; const int PricesPerMarket = 7; + const int RemoveCount = 3; //const int MarketCount = 101; //const int PricesPerMarket = 103; - const int RemoveCount = 53; + //const int RemoveCount = 53; const int ItemIdStride = 1000; const decimal BasePrice = 10m; const decimal PriceOffset = 10m; @@ -66,8 +67,6 @@ public void NullChecks() var actionParentCompare2 = () => emptyChangeSetObs.MergeManyChangeSets(nullSelector, sourceComparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); var actionParentCompareKey2a = () => nullChangeSetObs.MergeManyChangeSets(emptyKeySelector, sourceComparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); var actionParentCompareKey2b = () => emptyChangeSetObs.MergeManyChangeSets(nullKeySelector, sourceComparer: emptyParentComparer, equalityComparer: emptyEqualityComparer); - var actionParentCompareKey2c = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, sourceComparer: nullParentComparer, equalityComparer: emptyEqualityComparer); - var actionParentCompareKey2d = () => emptyChangeSetObs.MergeManyChangeSets(emptyKeySelector, sourceComparer: emptyParentComparer, equalityComparer: nullEqualityComparer); // then emptyChangeSetObs.Should().NotBeNull(); @@ -91,8 +90,6 @@ public void NullChecks() actionParentCompare2.Should().Throw(); actionParentCompareKey2a.Should().Throw(); actionParentCompareKey2b.Should().Throw(); - actionParentCompareKey2c.Should().Throw(); - actionParentCompareKey2d.Should().Throw(); } [Fact] @@ -135,13 +132,12 @@ IObservable> factory(IMarket m, Guid g) invoked.Should().BeTrue(); } -#if false [Fact] public void AllExistingSubItemsPresentInResult() { // having var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = ChangeSetByRating().AsAggregator(); markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when @@ -162,7 +158,7 @@ public void AllNewSubItemsPresentInResult() { // having var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = ChangeSetByRating().AsAggregator(); _marketCache.AddOrUpdate(markets); // when @@ -183,7 +179,7 @@ public void AllRefreshedSubItemsAreRefreshed() { // having var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = ChangeSetByRating().AsAggregator(); _marketCache.AddOrUpdate(markets); markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); @@ -205,7 +201,8 @@ public void AnyDuplicateKeyValuesShouldBeHidden() { // having var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = ChangeSetByRating().AsAggregator(); + markets[0].Rating = 1.0; _marketCache.AddOrUpdate(markets); // when @@ -219,6 +216,7 @@ public void AnyDuplicateKeyValuesShouldBeHidden() results.Summary.Overall.Adds.Should().Be(PricesPerMarket); results.Summary.Overall.Removes.Should().Be(0); results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); } [Fact] @@ -226,7 +224,8 @@ public void AnyDuplicateValuesShouldBeNoOpWhenRemoved() { // having var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = ChangeSetByRating().AsAggregator(); + markets[0].Rating = 1.0; _marketCache.AddOrUpdate(markets); markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); @@ -241,6 +240,7 @@ public void AnyDuplicateValuesShouldBeNoOpWhenRemoved() results.Summary.Overall.Adds.Should().Be(PricesPerMarket); results.Summary.Overall.Removes.Should().Be(0); results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); } [Fact] @@ -248,7 +248,8 @@ public void AnyDuplicateValuesShouldBeUnhiddenWhenOtherIsRemoved() { // having var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = ChangeSetByRating().AsAggregator(); + markets[0].Rating = 1.0; _marketCache.AddOrUpdate(markets); markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); @@ -262,6 +263,10 @@ public void AnyDuplicateValuesShouldBeUnhiddenWhenOtherIsRemoved() results.Data.Items.Zip(markets[1].PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); results.Messages.Count.Should().Be(2); results.Messages[1].Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Refreshes.Should().Be(0); } [Fact] @@ -269,7 +274,8 @@ public void AnyDuplicateValuesShouldNotRefreshWhenHidden() { // having var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = ChangeSetByRating().AsAggregator(); + markets[0].Rating = 1.0; _marketCache.AddOrUpdate(markets); markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); @@ -282,6 +288,58 @@ public void AnyDuplicateValuesShouldNotRefreshWhenHidden() results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Refreshes.Should().Be(0); results.Data.Items.Zip(markets[0].PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); + } + + [Fact] + public void SourceRefreshGeneratesUpdatesAsNeeded() + { + // having + var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); + using var results = ChangeSetByRating().AsAggregator(); + markets[0].Rating = 1.0; + _marketCache.AddOrUpdate(markets); + markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + + // when + SetRating(markets[1], 2.0); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Data.Items.Zip(markets[1].PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Refreshes.Should().Be(0); + } + + [Fact] + public void SourceRefreshDoesNothingIfDisabled() + { + // having + var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); + using var results = ChangeSetByRating(resortOnRefresh: false).AsAggregator(); + markets[0].Rating = 1.0; + _marketCache.AddOrUpdate(markets); + markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + + // when + SetRating(markets[1], 2.0); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket); + results.Data.Items.Zip(markets[0].PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); } [Fact] @@ -289,7 +347,7 @@ public void AnyRemovedSubItemIsRemoved() { // having var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = ChangeSetByRating().AsAggregator(); _marketCache.AddOrUpdate(markets); markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); @@ -303,6 +361,8 @@ public void AnyRemovedSubItemIsRemoved() results.Messages[0].Adds.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); results.Summary.Overall.Removes.Should().Be(MarketCount * RemoveCount); + results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); } [Fact] @@ -310,7 +370,7 @@ public void AnySourceItemRemovedRemovesAllSourceValues() { // having var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = ChangeSetByRating().AsAggregator(); _marketCache.AddOrUpdate(markets); markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); @@ -322,13 +382,15 @@ public void AnySourceItemRemovedRemovesAllSourceValues() results.Data.Count.Should().Be((MarketCount - RemoveCount) * PricesPerMarket); results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); results.Summary.Overall.Removes.Should().Be(PricesPerMarket * RemoveCount); + results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); } [Fact] public void ChangingSourceByUpdateRemovesPreviousAndAddsNewValues() { // having - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = ChangeSetByRating(false).AsAggregator(); var market = new Market(0); market.AddRandomPrices(0, PricesPerMarket * 2, GetRandomPrice); _marketCache.AddOrUpdate(market); @@ -346,45 +408,78 @@ public void ChangingSourceByUpdateRemovesPreviousAndAddsNewValues() results.Summary.Overall.Removes.Should().Be(PricesPerMarket); results.Data.Items.Zip(updatedMarket.PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); } -#endif [Fact] - public void ComparerOnlyAddsBetterAddedValues() + public void ChangingSourceByUpdateRemovesPreviousAndEmitsBetterValues() + { + // having + using var results = ChangeSetByRating(false).AsAggregator(); + var market = new Market(0); + var marketWorse = new Market(1); + SetRating(marketWorse, -1); + market.AddRandomPrices(0, PricesPerMarket * 2, GetRandomPrice); + marketWorse.AddRandomPrices(0, PricesPerMarket * 2, GetRandomPrice); + _marketCache.AddOrUpdate(market); + _marketCache.AddOrUpdate(marketWorse); + + var updatedMarket = new Market(market); + updatedMarket.AddRandomPrices(PricesPerMarket, PricesPerMarket * 3, GetRandomPrice); + + // when + _marketCache.AddOrUpdate(updatedMarket); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + results.Data.Count.Should().Be(PricesPerMarket * 2); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket * 3); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(PricesPerMarket); + results.Data.Items.Zip(updatedMarket.PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); + } + + [Fact] + public void UpdatesToCorrectValueOnRemove() { // having - using var results = ChangeSetByRatingOnly(false).AsAggregator(); var marketOriginal = new Market(0); var marketBetter = new Market(1); + var marketBest = new Market(2); marketBetter.Rating = 1.0; + marketBest.Rating = 5.0; marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketBest.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); + _marketCache.AddOrUpdate(marketBest); + _marketCache.AddOrUpdate(marketBetter); + using var results = ChangeSetByRating(false).AsAggregator(); // when - _marketCache.AddOrUpdate(marketBetter); + _marketCache.Remove(marketBest); // then _marketCacheResults.Data.Count.Should().Be(2); results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); - results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); } [Fact] - public void ComparerOnlyAddsBetterExistingValues() + public void OnlyUpdatesOnDuplicateIfNewItemIsFromBetterParent() { // having + using var results = ChangeSetByRating(false).AsAggregator(); + using var resultsLow = ChangeSetByLowRating(false).AsAggregator(); var marketOriginal = new Market(0); var marketBetter = new Market(1); marketBetter.Rating = 1.0; marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); - _marketCache.AddOrUpdate(marketBetter); // when - using var results = ChangeSetByRatingOnly(false).DebugSpy("Rating").AsAggregator(); + _marketCache.AddOrUpdate(marketBetter); // then _marketCacheResults.Data.Count.Should().Be(2); @@ -392,45 +487,55 @@ public void ComparerOnlyAddsBetterExistingValues() results.Summary.Overall.Adds.Should().Be(PricesPerMarket); results.Summary.Overall.Updates.Should().Be(PricesPerMarket); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + resultsLow.Data.Count.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); } [Fact] - public void ComparerOnlyAddsBetterValuesOnSourceUpdate() + public void BestChoiceFromDuplicatesSelectedWhenChangeSetCreated() { // having - using var results = ChangeSetByRatingOnly(false).AsAggregator(); var marketOriginal = new Market(0); var marketBetter = new Market(1); marketBetter.Rating = 1.0; marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketBetter); // when - marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + using var results = ChangeSetByRating(false).AsAggregator(); + using var resultsLow = ChangeSetByLowRating(false).AsAggregator(); // then _marketCacheResults.Data.Count.Should().Be(2); results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); - results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Updates.Should().Be(0); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + resultsLow.Data.Count.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); } [Fact] - public void ComparerUpdatesToCorrectValueOnRefresh() + public void OnlyAddsBetterValuesOnSourceUpdate() { // having - using var results = ChangeSetByRatingOnly(true).AsAggregator(); + using var results = ChangeSetByRating(false).AsAggregator(); + using var resultsLow = ChangeSetByLowRating(false).AsAggregator(); var marketOriginal = new Market(0); var marketBetter = new Market(1); + marketBetter.Rating = 1.0; marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketBetter); // when - SetRating(marketBetter, 1.0); + marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // then _marketCacheResults.Data.Count.Should().Be(2); @@ -438,63 +543,77 @@ public void ComparerUpdatesToCorrectValueOnRefresh() results.Summary.Overall.Adds.Should().Be(PricesPerMarket); results.Summary.Overall.Updates.Should().Be(PricesPerMarket); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + resultsLow.Data.Count.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); } [Fact] - public void ComparerUpdatesToCorrectValueOnRemove() + public void UpdatesToCorrectValueOnRefresh() { // having + using var results = ChangeSetByRating(false).AsAggregator(); + using var resultsLow = ChangeSetByLowRating(false).AsAggregator(); var marketOriginal = new Market(0); var marketBetter = new Market(1); - var marketBest = new Market(2); - marketBetter.Rating = 1.0; - marketBest.Rating = 5.0; marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketBest.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); - _marketCache.AddOrUpdate(marketBest); _marketCache.AddOrUpdate(marketBetter); - using var results = ChangeSetByRatingOnly(false).DebugSpy("Results").AsAggregator(); // when - _marketCache.Remove(marketBest); + SetRating(marketBetter, 1.0); // then _marketCacheResults.Data.Count.Should().Be(2); results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); - results.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + resultsLow.Data.Count.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); } [Fact] - public void ComparerUpdatesToCorrectValueOnUpdate() + public void ChildComparerUpdatesToCorrectValueOnUpdate() { // having - using var results = ChangeSetByRatingOnly(false).DebugSpy("Rating").AsAggregator(); - using var resultsLow = ChangeSetByLowRating(false).DebugSpy("Rating Low").AsAggregator(); + using var resultsLow = ChangeSetByLowRating(false).AsAggregator(); + using var resultsLowPrice = ChangeSetByRatingThenLow(false).AsAggregator(); + using var resultsHighPrice = ChangeSetByRatingThenHigh(false).AsAggregator(); var marketOriginal = new Market(0); - var marketBetter = new Market(1); - marketBetter.Rating = 1.0; + var marketHighest = new Market(1); + var marketLowest = new Market(2); + marketLowest.Rating = marketHighest.Rating = 1.0; marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketHighest.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketLowest.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); - _marketCache.AddOrUpdate(marketBetter); + _marketCache.AddOrUpdate(marketHighest); + _marketCache.AddOrUpdate(marketLowest); // when - marketBetter.UpdateAllPrices(GetRandomPrice()); + marketLowest.UpdateAllPrices(LowestPrice); // then - _marketCacheResults.Data.Count.Should().Be(2); - results.Data.Count.Should().Be(PricesPerMarket); - results.Summary.Overall.Adds.Should().Be(PricesPerMarket); - results.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); - results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + _marketCacheResults.Data.Count.Should().Be(3); resultsLow.Data.Count.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Updates.Should().Be(0); resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); + + resultsLowPrice.Data.Count.Should().Be(PricesPerMarket); + resultsLowPrice.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsLowPrice.Summary.Overall.Updates.Should().Be(PricesPerMarket); + resultsLowPrice.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketLowest.Id)); + + resultsHighPrice.Data.Count.Should().Be(PricesPerMarket); + resultsHighPrice.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsHighPrice.Summary.Overall.Updates.Should().Be(0); + resultsHighPrice.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketHighest.Id)); } #if false @@ -561,13 +680,14 @@ public void ComparerOnlyRefreshesVisibleValues() highPriceResults.Summary.Overall.Refreshes.Should().Be(0); highPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); } +#endif [Fact] public void EqualityComparerHidesUpdatesWithoutChanges() { // having var market = new Market(0); - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + using var results = CreateChangeSet("Equality Compare", Market.RatingCompare, equalityComparer: MarketPrice.EqualityComparer, resortOnRefresh: true).AsAggregator(); market.UpdatePrices(0, PricesPerMarket, LowestPrice); _marketCache.AddOrUpdate(market); @@ -583,7 +703,6 @@ public void EqualityComparerHidesUpdatesWithoutChanges() results.Summary.Overall.Updates.Should().Be(0); results.Summary.Overall.Refreshes.Should().Be(0); } -#endif [Fact] public void EveryItemVisibleWhenSequenceCompletes() @@ -655,12 +774,12 @@ private IObservable> CreateChangeSet(string name, I .MergeManyChangeSets(m => m.LatestPrices.DebugSpy($"{name} [{m.Name} Prices]"), sourceComp ?? Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, equalityComparer, childCompare) .DebugSpy($"{name} [Results]"); - private IObservable> ChangeSetByRatingOnly(bool resortOnRefresh = true) => CreateChangeSet("Rating", resortOnRefresh: resortOnRefresh); - private IObservable> ChangeSetByLowRating(bool resortOnRefresh = true) => CreateChangeSet("Rating Low", Market.RatingCompare.Invert(), resortOnRefresh: resortOnRefresh); - //private IObservable> ChangeSetByRatingThenHigh(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices.DebugSpy($"Rating / High [{m.Name}]"), Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.HighPriceCompare); - //private IObservable> ChangeSetByRatingThenLow(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices.DebugSpy($"Rating / Low [{m.Name}]"), Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.LowPriceCompare); - //private IObservable> ChangeSetByRatingThenRecent(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices.DebugSpy($"Rating / Recent [{m.Id}]"), Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare); - //private IObservable> ChangeSetByRatingThenRecentNoTimestamp(bool resortOnRefresh = true) => _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices.DebugSpy($"Rating / Recent (No TS) [{m.Id}]"), Market.RatingCompare, resortOnSourceRefresh: resortOnRefresh, MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare); + private IObservable> ChangeSetByRating(bool resortOnRefresh = true) => CreateChangeSet("Rating", resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByLowRating(bool resortOnRefresh = true) => CreateChangeSet("Low Rating", Market.RatingCompare.Invert(), resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByRatingThenHigh(bool resortOnRefresh = true) => CreateChangeSet("Rating | High", Market.RatingCompare, MarketPrice.HighPriceCompare, resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByRatingThenLow(bool resortOnRefresh = true) => CreateChangeSet("Rating | Low", Market.RatingCompare, MarketPrice.LowPriceCompare, resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByRatingThenRecent(bool resortOnRefresh = true) => CreateChangeSet("Rating | Recent", Market.RatingCompare, MarketPrice.LatestPriceCompare, equalityComparer: MarketPrice.EqualityComparer, resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByRatingThenTimestamp(bool resortOnRefresh = true) => CreateChangeSet("Rating | Timestamp", Market.RatingCompare, MarketPrice.LatestPriceCompare, equalityComparer: MarketPrice.EqualityComparerWithTimeStamp, resortOnRefresh: resortOnRefresh); private IMarket SetRating(IMarket market, double newRating) { diff --git a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs index d9631ffed..844b55ccc 100644 --- a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs +++ b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs @@ -79,7 +79,7 @@ public IObservable> Run() }).Transform(entry => entry.Child); } - private class ParentChildEntry + private sealed class ParentChildEntry { public ParentChildEntry(TObject parent, TDestination child) { @@ -92,7 +92,7 @@ public ParentChildEntry(TObject parent, TDestination child) public TDestination Child { get; } } - private class ParentChildCompare : IComparer + private sealed class ParentChildCompare : Comparer { private readonly IComparer _comparerParent; private readonly IComparer _comparerChild; @@ -103,41 +103,47 @@ public ParentChildCompare(IComparer comparerParent, IComparer - (x is null && y is null) ? 0 - : (x is null) ? 1 - : (y is null) ? -1 - : _comparerParent.Compare(x.Parent, y.Parent) switch - { - 0 => _comparerChild.Compare(x.Child, x.Child), - int i => i, - }; + public override int Compare(ParentChildEntry? x, ParentChildEntry? y) => (x, y) switch + { + (not null, not null) => _comparerParent.Compare(x.Parent, y.Parent) switch + { + 0 => _comparerChild.Compare(x.Child, x.Child), + int i => i, + }, + (null, null) => 0, + (null, not null) => 1, + (not null, null) => -1, + }; } - private class ParentOnlyCompare : IComparer + private sealed class ParentOnlyCompare : Comparer { private readonly IComparer _comparerParent; public ParentOnlyCompare(IComparer comparer) => _comparerParent = comparer; - public int Compare(ParentChildEntry? x, ParentChildEntry? y) => - (x is null && y is null) ? 0 - : (x is null) ? 1 - : (y is null) ? -1 - : _comparerParent.Compare(x.Parent, y.Parent); + public override int Compare(ParentChildEntry? x, ParentChildEntry? y) => (x, y) switch + { + (not null, not null) => _comparerParent.Compare(x.Parent, x.Parent), + (null, null) => 0, + (null, not null) => 1, + (not null, null) => -1, + }; } - private class ParentChildEqualityCompare : IEqualityComparer + private sealed class ParentChildEqualityCompare : EqualityComparer { private readonly IEqualityComparer _comparer; public ParentChildEqualityCompare(IEqualityComparer comparer) => _comparer = comparer; - public bool Equals(ParentChildEntry? x, ParentChildEntry? y) => - (x is null && y is null) ? true - : (x is null || y is null) ? false - : _comparer.Equals(x.Child, y.Child); + public override bool Equals(ParentChildEntry? x, ParentChildEntry? y) => (x, y) switch + { + (not null, not null) => _comparer.Equals(x.Child, y.Child), + (null, null) => true, + _ => false, + }; - public int GetHashCode(ParentChildEntry obj) => _comparer.GetHashCode(obj.Child); + public override int GetHashCode(ParentChildEntry obj) => _comparer.GetHashCode(obj.Child); } } From ea69497ff20dd95d8a6a7f0c2b44e814be9cc1c4 Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Mon, 6 Nov 2023 00:12:09 -0800 Subject: [PATCH 7/8] More unit tests --- .../Cache/MergeChangeSetsFixture.cs | 36 +++++++++---------- .../Cache/MergeManyCacheChangeSetsFixture.cs | 28 +++++++-------- ...ManyCacheChangeSetsSourceCompareFixture.cs | 8 ++--- src/DynamicData.Tests/Domain/Market.cs | 6 +++- .../MergeManyCacheChangeSetsSourceCompare.cs | 4 +-- 5 files changed, 43 insertions(+), 39 deletions(-) diff --git a/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs b/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs index 7e76a62a4..c7b33f23c 100644 --- a/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs @@ -324,8 +324,8 @@ public void ComparerOnlyAddsBetterAddedValues() marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); // when - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); - marketHigh.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); + marketHigh.SetPrices(0, PricesPerMarket, HighestPrice); // then _marketList.Count.Should().Be(3); @@ -348,8 +348,8 @@ public void ComparerOnlyAddsBetterExistingValues() var marketHigh = Add(new Market(2)); var others = new[] { marketLow.LatestPrices, marketHigh.LatestPrices }; marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); - marketHigh.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); + marketHigh.SetPrices(0, PricesPerMarket, HighestPrice); // when using var highPriceResults = marketOriginal.LatestPrices.MergeChangeSets(others, MarketPrice.HighPriceCompare).AsAggregator(); @@ -376,7 +376,7 @@ public void ComparerUpdatesToCorrectValueOnRefresh() using var highPriceResults = marketOriginal.LatestPrices.MergeChangeSets(marketFlipFlop.LatestPrices, MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = marketOriginal.LatestPrices.MergeChangeSets(marketFlipFlop.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketFlipFlop.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketFlipFlop.SetPrices(0, PricesPerMarket, HighestPrice); // when marketFlipFlop.RefreshAllPrices(LowestPrice); @@ -408,8 +408,8 @@ public void ComparerUpdatesToCorrectValueOnRemove() using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.LowPriceCompare).AsAggregator(); using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.HighPriceCompare).AsAggregator(); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); - marketHigh.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); + marketHigh.SetPrices(0, PricesPerMarket, HighestPrice); // when marketLow.RemoveAllPrices(); @@ -441,7 +441,7 @@ public void ComparerUpdatesToCorrectValueOnUpdate() using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.LowPriceCompare).AsAggregator(); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketFlipFlop.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketFlipFlop.SetPrices(0, PricesPerMarket, HighestPrice); // when marketFlipFlop.UpdateAllPrices(LowestPrice); @@ -471,7 +471,7 @@ public void ComparerOnlyUpdatesVisibleValuesOnUpdate() using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.LowPriceCompare).AsAggregator(); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); // when marketLow.UpdateAllPrices(LowestPrice - 1); @@ -501,7 +501,7 @@ public void ComparerOnlyRefreshesVisibleValues() using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.LowPriceCompare).AsAggregator(); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); // when marketLow.RefreshAllPrices(LowestPrice - 1); @@ -576,10 +576,10 @@ public void EqualityComparerHidesUpdatesWithoutChanges() // having var market = Add(new Market(0)); using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); - market.UpdatePrices(0, PricesPerMarket, LowestPrice); + market.SetPrices(0, PricesPerMarket, LowestPrice); // when - market.UpdatePrices(0, PricesPerMarket, LowestPrice); + market.SetPrices(0, PricesPerMarket, LowestPrice); // then _marketList.Count.Should().Be(1); @@ -601,10 +601,10 @@ public void EqualityComparerAndComparerWorkTogetherForUpdates() var results = market1.LatestPrices.MergeChangeSets(market2.LatestPrices, MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); var resultsTimeStamp = market1.LatestPrices.MergeChangeSets(market2.LatestPrices, MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); market1.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - market2.UpdatePrices(0, PricesPerMarket, LowestPrice); + market2.SetPrices(0, PricesPerMarket, LowestPrice); // when - market2.UpdatePrices(0, PricesPerMarket, LowestPrice); + market2.SetPrices(0, PricesPerMarket, LowestPrice); // then _marketList.Count.Should().Be(2); @@ -631,9 +631,9 @@ public void EqualityComparerAndComparerWorkTogetherForRefreshes() var results1 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); var results2 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); market1.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - market2.UpdatePrices(0, PricesPerMarket, LowestPrice); + market2.SetPrices(0, PricesPerMarket, LowestPrice); // Update again, but only the timestamp will change, so results1 will ignore - market2.UpdatePrices(0, PricesPerMarket, LowestPrice); + market2.SetPrices(0, PricesPerMarket, LowestPrice); // when // results1 won't see the refresh because it ignored the update @@ -665,9 +665,9 @@ public void EqualityComparerAndComparerRefreshesBecomeUpdates() var results1 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); var results2 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); market1.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - market2.UpdatePrices(0, PricesPerMarket, LowestPrice - 1); + market2.SetPrices(0, PricesPerMarket, LowestPrice - 1); // Update again, but only the timestamp will change, so results1 will ignore - market2.UpdatePrices(0, PricesPerMarket, LowestPrice - 1); + market2.SetPrices(0, PricesPerMarket, LowestPrice - 1); // when // results1 will see this as an update because it ignored the last update diff --git a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs index 21ba0650a..9b4bdaff2 100644 --- a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs @@ -355,8 +355,8 @@ public void ComparerOnlyAddsBetterAddedValues() _marketCache.AddOrUpdate(marketHigh); // when - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); - marketHigh.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); + marketHigh.SetPrices(0, PricesPerMarket, HighestPrice); // then _marketCacheResults.Data.Count.Should().Be(3); @@ -381,8 +381,8 @@ public void ComparerOnlyAddsBetterExistingValues() var marketHigh = new Market(2); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); - marketHigh.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); + marketHigh.SetPrices(0, PricesPerMarket, HighestPrice); // when _marketCache.AddOrUpdate(marketLow); @@ -410,8 +410,8 @@ public void ComparerOnlyAddsBetterValuesOnSourceUpdate() var marketLow = new Market(1); var marketLowLow = new Market(marketLow); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); - marketLowLow.UpdatePrices(0, PricesPerMarket, LowestPrice - 1); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); + marketLowLow.SetPrices(0, PricesPerMarket, LowestPrice - 1); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); @@ -441,7 +441,7 @@ public void ComparerUpdatesToCorrectValueOnRefresh() var marketOriginal = new Market(0); var marketFlipFlop = new Market(1); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketFlipFlop.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketFlipFlop.SetPrices(0, PricesPerMarket, HighestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketFlipFlop); @@ -478,8 +478,8 @@ public void ComparerUpdatesToCorrectValueOnRemove() _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); _marketCache.AddOrUpdate(marketHigh); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); - marketHigh.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); + marketHigh.SetPrices(0, PricesPerMarket, HighestPrice); // when _marketCache.Remove(marketLow); @@ -511,7 +511,7 @@ public void ComparerUpdatesToCorrectValueOnUpdate() var marketOriginal = new Market(0); var marketFlipFlop = new Market(1); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketFlipFlop.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketFlipFlop.SetPrices(0, PricesPerMarket, HighestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketFlipFlop); @@ -543,7 +543,7 @@ public void ComparerOnlyUpdatesVisibleValuesOnUpdate() var marketOriginal = new Market(0); var marketLow = new Market(1); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); @@ -575,7 +575,7 @@ public void ComparerOnlyRefreshesVisibleValues() var marketOriginal = new Market(0); var marketLow = new Market(1); marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); @@ -604,11 +604,11 @@ public void EqualityComparerHidesUpdatesWithoutChanges() // having var market = new Market(0); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); - market.UpdatePrices(0, PricesPerMarket, LowestPrice); + market.SetPrices(0, PricesPerMarket, LowestPrice); _marketCache.AddOrUpdate(market); // when - market.UpdatePrices(0, PricesPerMarket, LowestPrice); + market.SetPrices(0, PricesPerMarket, LowestPrice); // then _marketCacheResults.Data.Count.Should().Be(1); diff --git a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs index a11e2afc5..de5b52cd7 100644 --- a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs @@ -513,7 +513,7 @@ public void BestChoiceFromDuplicatesSelectedWhenChangeSetCreated() _marketCacheResults.Data.Count.Should().Be(2); results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); - results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); resultsLow.Data.Count.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); @@ -589,7 +589,7 @@ public void ChildComparerUpdatesToCorrectValueOnUpdate() var marketLowest = new Market(2); marketLowest.Rating = marketHighest.Rating = 1.0; marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketHighest.UpdatePrices(0, PricesPerMarket, HighestPrice); + marketHighest.SetPrices(0, PricesPerMarket, HighestPrice); marketLowest.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketHighest); @@ -688,11 +688,11 @@ public void EqualityComparerHidesUpdatesWithoutChanges() // having var market = new Market(0); using var results = CreateChangeSet("Equality Compare", Market.RatingCompare, equalityComparer: MarketPrice.EqualityComparer, resortOnRefresh: true).AsAggregator(); - market.UpdatePrices(0, PricesPerMarket, LowestPrice); + market.SetPrices(0, PricesPerMarket, LowestPrice); _marketCache.AddOrUpdate(market); // when - market.UpdatePrices(0, PricesPerMarket, LowestPrice); + market.SetPrices(0, PricesPerMarket, LowestPrice); // then _marketCacheResults.Data.Count.Should().Be(1); diff --git a/src/DynamicData.Tests/Domain/Market.cs b/src/DynamicData.Tests/Domain/Market.cs index b546205ab..f28673a9c 100644 --- a/src/DynamicData.Tests/Domain/Market.cs +++ b/src/DynamicData.Tests/Domain/Market.cs @@ -95,10 +95,12 @@ public Market RefreshPrice(int id, decimal newPrice) public Market UpdateAllPrices(decimal newPrice) => this.With(_ => _latestPrices.Edit(updater => updater.AddOrUpdate(updater.Items.Select(cp => CreatePrice(cp.ItemId, newPrice))))); - public Market UpdatePrices(int minId, int maxId, decimal newPrice) => this.With(_ => _latestPrices.AddOrUpdate(Enumerable.Range(minId, maxId - minId).Select(id => CreatePrice(id, newPrice)))); + public Market SetPrices(int minId, int maxId, decimal newPrice) => this.With(_ => _latestPrices.AddOrUpdate(Enumerable.Range(minId, maxId - minId).Select(id => CreatePrice(id, newPrice)))); public void Dispose() => _latestPrices.Dispose(); + public override string ToString() => $"Market '{Name}' [{Id}] (Rating: {Rating})"; + private class RatingComparer : IComparer { public int Compare([DisallowNull] IMarket x, [DisallowNull] IMarket y) @@ -127,4 +129,6 @@ public FixedMarket(Func getPrice, int minId, int maxId, bool completabl public double Rating { get; set; } public Guid Id { get; } + + public override string ToString() => $"Fixed Market '{Name}' (Rating: {Rating})"; } diff --git a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs index 844b55ccc..c9f56e30b 100644 --- a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs +++ b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs @@ -107,7 +107,7 @@ public ParentChildCompare(IComparer comparerParent, IComparer _comparerParent.Compare(x.Parent, y.Parent) switch { - 0 => _comparerChild.Compare(x.Child, x.Child), + 0 => _comparerChild.Compare(x.Child, y.Child), int i => i, }, (null, null) => 0, @@ -124,7 +124,7 @@ private sealed class ParentOnlyCompare : Comparer public override int Compare(ParentChildEntry? x, ParentChildEntry? y) => (x, y) switch { - (not null, not null) => _comparerParent.Compare(x.Parent, x.Parent), + (not null, not null) => _comparerParent.Compare(x.Parent, y.Parent), (null, null) => 0, (null, not null) => 1, (not null, null) => -1, From cf5f071250c2d1d7ffebc4792b6564f0347a339f Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Mon, 6 Nov 2023 12:10:13 -0800 Subject: [PATCH 8/8] Finalized Unit Tests --- .../Cache/MergeChangeSetsFixture.cs | 44 +- .../Cache/MergeManyCacheChangeSetsFixture.cs | 58 +-- ...ManyCacheChangeSetsSourceCompareFixture.cs | 394 +++++++++++++----- src/DynamicData.Tests/Domain/Market.cs | 49 ++- src/DynamicData/Cache/ObservableCacheEx.cs | 2 +- 5 files changed, 376 insertions(+), 171 deletions(-) diff --git a/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs b/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs index c7b33f23c..e9bb54e0c 100644 --- a/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs @@ -7,15 +7,23 @@ using DynamicData.Tests.Utilities; using FluentAssertions; using Microsoft.Reactive.Testing; + using Xunit; namespace DynamicData.Tests.Cache; public sealed partial class MergeChangeSetsFixture : IDisposable { +#if DEBUG + const int MarketCount = 5; + const int PricesPerMarket = 7; + const int RemoveCount = 3; +#else const int MarketCount = 101; const int PricesPerMarket = 103; const int RemoveCount = 53; +#endif + const int ItemIdStride = 1000; const decimal BasePrice = 10m; const decimal PriceOffset = 10m; @@ -220,8 +228,8 @@ public void AnyDuplicateKeyValuesShouldBeHidden() using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); // when - _marketList[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - _marketList[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // then _marketList.Count.Should().Be(2); @@ -238,8 +246,8 @@ public void AnyDuplicateValuesShouldBeNoOpWhenRemoved() // having _marketList.AddRange(Enumerable.Range(0, 2).Select(n => new Market(n))); using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); - _marketList[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - _marketList[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when _marketList[1].RemoveAllPrices(); @@ -259,8 +267,8 @@ public void AnyDuplicateValuesShouldBeUnhiddenWhenOtherIsRemoved() // having _marketList.AddRange(Enumerable.Range(0, 2).Select(n => new Market(n))); using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); - _marketList[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - _marketList[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when _marketList[0].RemoveAllPrices(); @@ -278,8 +286,8 @@ public void AnyDuplicateValuesShouldNotRefreshWhenHidden() // having _marketList.AddRange(Enumerable.Range(0, 2).Select(n => new Market(n))); using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); - _marketList[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - _marketList[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + _marketList[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when _marketList[1].RefreshAllPrices(GetRandomPrice); @@ -321,7 +329,7 @@ public void ComparerOnlyAddsBetterAddedValues() var others = new[] { marketLow.LatestPrices, marketHigh.LatestPrices }; using var highPriceResults = marketOriginal.LatestPrices.MergeChangeSets(others, MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = marketOriginal.LatestPrices.MergeChangeSets(others, MarketPrice.LowPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); // when marketLow.SetPrices(0, PricesPerMarket, LowestPrice); @@ -347,7 +355,7 @@ public void ComparerOnlyAddsBetterExistingValues() var marketLow = Add(new Market(1)); var marketHigh = Add(new Market(2)); var others = new[] { marketLow.LatestPrices, marketHigh.LatestPrices }; - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketLow.SetPrices(0, PricesPerMarket, LowestPrice); marketHigh.SetPrices(0, PricesPerMarket, HighestPrice); @@ -375,7 +383,7 @@ public void ComparerUpdatesToCorrectValueOnRefresh() var marketFlipFlop = Add(new Market(1)); using var highPriceResults = marketOriginal.LatestPrices.MergeChangeSets(marketFlipFlop.LatestPrices, MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = marketOriginal.LatestPrices.MergeChangeSets(marketFlipFlop.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketFlipFlop.SetPrices(0, PricesPerMarket, HighestPrice); // when @@ -407,7 +415,7 @@ public void ComparerUpdatesToCorrectValueOnRemove() using var results = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.LowPriceCompare).AsAggregator(); using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.HighPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketLow.SetPrices(0, PricesPerMarket, LowestPrice); marketHigh.SetPrices(0, PricesPerMarket, HighestPrice); @@ -440,7 +448,7 @@ public void ComparerUpdatesToCorrectValueOnUpdate() var marketFlipFlop = Add(new Market(1)); using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.LowPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketFlipFlop.SetPrices(0, PricesPerMarket, HighestPrice); // when @@ -470,7 +478,7 @@ public void ComparerOnlyUpdatesVisibleValuesOnUpdate() var marketLow = Add(new Market(1)); using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.LowPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketLow.SetPrices(0, PricesPerMarket, LowestPrice); // when @@ -500,7 +508,7 @@ public void ComparerOnlyRefreshesVisibleValues() var marketLow = Add(new Market(1)); using var highPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.HighPriceCompare).AsAggregator(); using var lowPriceResults = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.LowPriceCompare).AsAggregator(); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketLow.SetPrices(0, PricesPerMarket, LowestPrice); // when @@ -600,7 +608,7 @@ public void EqualityComparerAndComparerWorkTogetherForUpdates() var results = market1.LatestPrices.MergeChangeSets(market2.LatestPrices, MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); var resultsTimeStamp = market1.LatestPrices.MergeChangeSets(market2.LatestPrices, MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); - market1.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + market1.SetPrices(0, PricesPerMarket, GetRandomPrice); market2.SetPrices(0, PricesPerMarket, LowestPrice); // when @@ -630,7 +638,7 @@ public void EqualityComparerAndComparerWorkTogetherForRefreshes() var results1 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); var results2 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); - market1.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + market1.SetPrices(0, PricesPerMarket, GetRandomPrice); market2.SetPrices(0, PricesPerMarket, LowestPrice); // Update again, but only the timestamp will change, so results1 will ignore market2.SetPrices(0, PricesPerMarket, LowestPrice); @@ -664,7 +672,7 @@ public void EqualityComparerAndComparerRefreshesBecomeUpdates() var results1 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparer, MarketPrice.LatestPriceCompare).AsAggregator(); var results2 = _marketList.Select(m => m.LatestPrices).MergeChangeSets(MarketPrice.EqualityComparerWithTimeStamp, MarketPrice.LatestPriceCompare).AsAggregator(); - market1.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + market1.SetPrices(0, PricesPerMarket, GetRandomPrice); market2.SetPrices(0, PricesPerMarket, LowestPrice - 1); // Update again, but only the timestamp will change, so results1 will ignore market2.SetPrices(0, PricesPerMarket, LowestPrice - 1); diff --git a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs index 9b4bdaff2..ca3396c0d 100644 --- a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsFixture.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Linq; using DynamicData.Kernel; @@ -10,22 +8,28 @@ using FluentAssertions; using Xunit; -using Xunit.Sdk; namespace DynamicData.Tests.Cache; public sealed class MergeManyCacheChangeSetsFixture : IDisposable { +#if DEBUG + const int MarketCount = 5; + const int PricesPerMarket = 7; + const int RemoveCount = 3; +#else const int MarketCount = 101; const int PricesPerMarket = 103; const int RemoveCount = 53; +#endif + const int ItemIdStride = 1000; const decimal BasePrice = 10m; const decimal PriceOffset = 10m; const decimal HighestPrice = BasePrice + PriceOffset + 1.0m; const decimal LowestPrice = BasePrice - 1.0m; - private static readonly Random Random = new Random(0x21123737); + private static readonly Random Random = new (0x21123737); private static decimal GetRandomPrice() => MarketPrice.RandomPrice(Random, BasePrice, PriceOffset); @@ -135,7 +139,7 @@ public void AllExistingSubItemsPresentInResult() // having var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when _marketCache.AddOrUpdate(markets); @@ -159,7 +163,7 @@ public void AllNewSubItemsPresentInResult() _marketCache.AddOrUpdate(markets); // when - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // then _marketCacheResults.Data.Count.Should().Be(MarketCount); @@ -178,7 +182,7 @@ public void AllRefreshedSubItemsAreRefreshed() var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when markets.ForEach(m => m.RefreshAllPrices(GetRandomPrice)); @@ -202,8 +206,8 @@ public void AnyDuplicateKeyValuesShouldBeHidden() _marketCache.AddOrUpdate(markets); // when - markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // then _marketCacheResults.Data.Count.Should().Be(2); @@ -221,8 +225,8 @@ public void AnyDuplicateValuesShouldBeNoOpWhenRemoved() var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when markets[1].RemoveAllPrices(); @@ -243,8 +247,8 @@ public void AnyDuplicateValuesShouldBeUnhiddenWhenOtherIsRemoved() var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when _marketCache.Remove(markets[0]); @@ -264,8 +268,8 @@ public void AnyDuplicateValuesShouldNotRefreshWhenHidden() var markets = Enumerable.Range(0, 2).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when markets[1].RefreshAllPrices(GetRandomPrice); @@ -284,7 +288,7 @@ public void AnyRemovedSubItemIsRemoved() var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when markets.ForEach(m => m.PricesCache.Edit(updater => updater.RemoveKeys(updater.Keys.Take(RemoveCount)))); @@ -305,7 +309,7 @@ public void AnySourceItemRemovedRemovesAllSourceValues() var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); _marketCache.AddOrUpdate(markets); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when _marketCache.Edit(updater => updater.RemoveKeys(updater.Keys.Take(RemoveCount))); @@ -323,10 +327,10 @@ public void ChangingSourceByUpdateRemovesPreviousAndAddsNewValues() // having using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); var market = new Market(0); - market.AddRandomPrices(0, PricesPerMarket * 2, GetRandomPrice); + market.SetPrices(0, PricesPerMarket * 2, GetRandomPrice); _marketCache.AddOrUpdate(market); var updatedMarket = new Market(market); - updatedMarket.AddRandomPrices(PricesPerMarket, PricesPerMarket * 3, GetRandomPrice); + updatedMarket.SetPrices(PricesPerMarket, PricesPerMarket * 3, GetRandomPrice); // when _marketCache.AddOrUpdate(updatedMarket); @@ -349,7 +353,7 @@ public void ComparerOnlyAddsBetterAddedValues() var marketOriginal = new Market(0); var marketLow = new Market(1); var marketHigh = new Market(2); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); _marketCache.AddOrUpdate(marketHigh); @@ -379,7 +383,7 @@ public void ComparerOnlyAddsBetterExistingValues() var marketOriginal = new Market(0); var marketLow = new Market(1); var marketHigh = new Market(2); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); marketLow.SetPrices(0, PricesPerMarket, LowestPrice); marketHigh.SetPrices(0, PricesPerMarket, HighestPrice); @@ -409,7 +413,7 @@ public void ComparerOnlyAddsBetterValuesOnSourceUpdate() var marketOriginal = new Market(0); var marketLow = new Market(1); var marketLowLow = new Market(marketLow); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketLow.SetPrices(0, PricesPerMarket, LowestPrice); marketLowLow.SetPrices(0, PricesPerMarket, LowestPrice - 1); _marketCache.AddOrUpdate(marketOriginal); @@ -440,7 +444,7 @@ public void ComparerUpdatesToCorrectValueOnRefresh() using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); var marketOriginal = new Market(0); var marketFlipFlop = new Market(1); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketFlipFlop.SetPrices(0, PricesPerMarket, HighestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketFlipFlop); @@ -474,7 +478,7 @@ public void ComparerUpdatesToCorrectValueOnRemove() var marketOriginal = new Market(0); var marketLow = new Market(1); var marketHigh = new Market(2); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); _marketCache.AddOrUpdate(marketHigh); @@ -510,7 +514,7 @@ public void ComparerUpdatesToCorrectValueOnUpdate() using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); var marketOriginal = new Market(0); var marketFlipFlop = new Market(1); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketFlipFlop.SetPrices(0, PricesPerMarket, HighestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketFlipFlop); @@ -542,7 +546,7 @@ public void ComparerOnlyUpdatesVisibleValuesOnUpdate() using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); var marketOriginal = new Market(0); var marketLow = new Market(1); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketLow.SetPrices(0, PricesPerMarket, LowestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); @@ -574,7 +578,7 @@ public void ComparerOnlyRefreshesVisibleValues() using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer, MarketPrice.LowPriceCompare).AsAggregator(); var marketOriginal = new Market(0); var marketLow = new Market(1); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketLow.SetPrices(0, PricesPerMarket, LowestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); diff --git a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs index de5b52cd7..e3fd02908 100644 --- a/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyCacheChangeSetsSourceCompareFixture.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Linq; using DynamicData.Kernel; @@ -10,25 +8,28 @@ using FluentAssertions; using Xunit; -using Xunit.Sdk; namespace DynamicData.Tests.Cache; public sealed class MergeManyCacheChangeSetsSourceCompareFixture : IDisposable { +#if DEBUG const int MarketCount = 5; const int PricesPerMarket = 7; const int RemoveCount = 3; - //const int MarketCount = 101; - //const int PricesPerMarket = 103; - //const int RemoveCount = 53; +#else + const int MarketCount = 101; + const int PricesPerMarket = 103; + const int RemoveCount = 53; +#endif + const int ItemIdStride = 1000; const decimal BasePrice = 10m; const decimal PriceOffset = 10m; const decimal HighestPrice = BasePrice + PriceOffset + 1.0m; const decimal LowestPrice = BasePrice - 1.0m; - private static readonly Random Random = new Random(0x21123737); + private static readonly Random Random = new (0x10012022); private static decimal GetRandomPrice() => MarketPrice.RandomPrice(Random, BasePrice, PriceOffset); @@ -138,7 +139,7 @@ public void AllExistingSubItemsPresentInResult() // having var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = ChangeSetByRating().AsAggregator(); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when _marketCache.AddOrUpdate(markets); @@ -151,6 +152,7 @@ public void AllExistingSubItemsPresentInResult() results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); results.Summary.Overall.Removes.Should().Be(0); results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); } [Fact] @@ -162,7 +164,7 @@ public void AllNewSubItemsPresentInResult() _marketCache.AddOrUpdate(markets); // when - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // then _marketCacheResults.Data.Count.Should().Be(MarketCount); @@ -172,6 +174,7 @@ public void AllNewSubItemsPresentInResult() results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); results.Summary.Overall.Removes.Should().Be(0); results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); } [Fact] @@ -181,7 +184,7 @@ public void AllRefreshedSubItemsAreRefreshed() var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = ChangeSetByRating().AsAggregator(); _marketCache.AddOrUpdate(markets); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when markets.ForEach(m => m.RefreshAllPrices(GetRandomPrice)); @@ -206,8 +209,8 @@ public void AnyDuplicateKeyValuesShouldBeHidden() _marketCache.AddOrUpdate(markets); // when - markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // then _marketCacheResults.Data.Count.Should().Be(2); @@ -227,8 +230,8 @@ public void AnyDuplicateValuesShouldBeNoOpWhenRemoved() using var results = ChangeSetByRating().AsAggregator(); markets[0].Rating = 1.0; _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when markets[1].RemoveAllPrices(); @@ -251,8 +254,8 @@ public void AnyDuplicateValuesShouldBeUnhiddenWhenOtherIsRemoved() using var results = ChangeSetByRating().AsAggregator(); markets[0].Rating = 1.0; _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when _marketCache.Remove(markets[0]); @@ -277,8 +280,8 @@ public void AnyDuplicateValuesShouldNotRefreshWhenHidden() using var results = ChangeSetByRating().AsAggregator(); markets[0].Rating = 1.0; _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when markets[1].RefreshAllPrices(GetRandomPrice); @@ -302,8 +305,8 @@ public void SourceRefreshGeneratesUpdatesAsNeeded() using var results = ChangeSetByRating().AsAggregator(); markets[0].Rating = 1.0; _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when SetRating(markets[1], 2.0); @@ -326,8 +329,8 @@ public void SourceRefreshDoesNothingIfDisabled() using var results = ChangeSetByRating(resortOnRefresh: false).AsAggregator(); markets[0].Rating = 1.0; _marketCache.AddOrUpdate(markets); - markets[0].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - markets[1].AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + markets[0].SetPrices(0, PricesPerMarket, GetRandomPrice); + markets[1].SetPrices(0, PricesPerMarket, GetRandomPrice); // when SetRating(markets[1], 2.0); @@ -349,7 +352,7 @@ public void AnyRemovedSubItemIsRemoved() var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = ChangeSetByRating().AsAggregator(); _marketCache.AddOrUpdate(markets); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when markets.ForEach(m => m.PricesCache.Edit(updater => updater.RemoveKeys(updater.Keys.Take(RemoveCount)))); @@ -372,7 +375,7 @@ public void AnySourceItemRemovedRemovesAllSourceValues() var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = ChangeSetByRating().AsAggregator(); _marketCache.AddOrUpdate(markets); - markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.AddRandomPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); // when _marketCache.Edit(updater => updater.RemoveKeys(updater.Keys.Take(RemoveCount))); @@ -392,10 +395,10 @@ public void ChangingSourceByUpdateRemovesPreviousAndAddsNewValues() // having using var results = ChangeSetByRating(false).AsAggregator(); var market = new Market(0); - market.AddRandomPrices(0, PricesPerMarket * 2, GetRandomPrice); + market.SetPrices(0, PricesPerMarket * 2, GetRandomPrice); _marketCache.AddOrUpdate(market); var updatedMarket = new Market(market); - updatedMarket.AddRandomPrices(PricesPerMarket, PricesPerMarket * 3, GetRandomPrice); + updatedMarket.SetPrices(PricesPerMarket, PricesPerMarket * 3, GetRandomPrice); // when _marketCache.AddOrUpdate(updatedMarket); @@ -406,6 +409,7 @@ public void ChangingSourceByUpdateRemovesPreviousAndAddsNewValues() results.Summary.Overall.Adds.Should().Be(PricesPerMarket * 3); results.Summary.Overall.Updates.Should().Be(PricesPerMarket); results.Summary.Overall.Removes.Should().Be(PricesPerMarket); + results.Summary.Overall.Refreshes.Should().Be(0); results.Data.Items.Zip(updatedMarket.PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); } @@ -417,24 +421,26 @@ public void ChangingSourceByUpdateRemovesPreviousAndEmitsBetterValues() var market = new Market(0); var marketWorse = new Market(1); SetRating(marketWorse, -1); - market.AddRandomPrices(0, PricesPerMarket * 2, GetRandomPrice); - marketWorse.AddRandomPrices(0, PricesPerMarket * 2, GetRandomPrice); + market.SetPrices(0, PricesPerMarket * 2, GetRandomPrice); + marketWorse.SetPrices(0, PricesPerMarket * 2, GetRandomPrice); _marketCache.AddOrUpdate(market); _marketCache.AddOrUpdate(marketWorse); var updatedMarket = new Market(market); - updatedMarket.AddRandomPrices(PricesPerMarket, PricesPerMarket * 3, GetRandomPrice); + updatedMarket.SetPrices(PricesPerMarket, PricesPerMarket * 3, GetRandomPrice); // when _marketCache.AddOrUpdate(updatedMarket); // then _marketCacheResults.Data.Count.Should().Be(2); - results.Data.Count.Should().Be(PricesPerMarket * 2); + results.Data.Count.Should().Be(PricesPerMarket * 3); results.Summary.Overall.Adds.Should().Be(PricesPerMarket * 3); - results.Summary.Overall.Updates.Should().Be(PricesPerMarket); - results.Summary.Overall.Removes.Should().Be(PricesPerMarket); - results.Data.Items.Zip(updatedMarket.PricesCache.Items).ForEach(pair => pair.First.Should().Be(pair.Second)); + results.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); + results.Data.Items.Take(PricesPerMarket).Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketWorse.Id)); + results.Data.Items.Skip(PricesPerMarket).Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(updatedMarket.Id)); } [Fact] @@ -446,9 +452,9 @@ public void UpdatesToCorrectValueOnRemove() var marketBest = new Market(2); marketBetter.Rating = 1.0; marketBest.Rating = 5.0; - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketBest.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.SetPrices(0, PricesPerMarket, GetRandomPrice); + marketBest.SetPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketBest); _marketCache.AddOrUpdate(marketBetter); @@ -462,6 +468,8 @@ public void UpdatesToCorrectValueOnRemove() results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); results.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); } @@ -474,8 +482,8 @@ public void OnlyUpdatesOnDuplicateIfNewItemIsFromBetterParent() var marketOriginal = new Market(0); var marketBetter = new Market(1); marketBetter.Rating = 1.0; - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.SetPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); // when @@ -486,10 +494,14 @@ public void OnlyUpdatesOnDuplicateIfNewItemIsFromBetterParent() results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); resultsLow.Data.Count.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Summary.Overall.Removes.Should().Be(0); + resultsLow.Summary.Overall.Refreshes.Should().Be(0); resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); } @@ -500,8 +512,8 @@ public void BestChoiceFromDuplicatesSelectedWhenChangeSetCreated() var marketOriginal = new Market(0); var marketBetter = new Market(1); marketBetter.Rating = 1.0; - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.SetPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketBetter); @@ -514,10 +526,14 @@ public void BestChoiceFromDuplicatesSelectedWhenChangeSetCreated() results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); resultsLow.Data.Count.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Summary.Overall.Removes.Should().Be(0); + resultsLow.Summary.Overall.Refreshes.Should().Be(0); resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); } @@ -530,22 +546,26 @@ public void OnlyAddsBetterValuesOnSourceUpdate() var marketOriginal = new Market(0); var marketBetter = new Market(1); marketBetter.Rating = 1.0; - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketBetter); // when - marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.SetPrices(0, PricesPerMarket, GetRandomPrice); // then _marketCacheResults.Data.Count.Should().Be(2); results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); results.Summary.Overall.Updates.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); resultsLow.Data.Count.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Summary.Overall.Removes.Should().Be(0); + resultsLow.Summary.Overall.Refreshes.Should().Be(0); resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); } @@ -555,26 +575,45 @@ public void UpdatesToCorrectValueOnRefresh() // having using var results = ChangeSetByRating(false).AsAggregator(); using var resultsLow = ChangeSetByLowRating(false).AsAggregator(); + using var resultsRefresh = ChangeSetByRating(true).AsAggregator(); + using var resultsLowRefresh = ChangeSetByLowRating(true).AsAggregator(); var marketOriginal = new Market(0); var marketBetter = new Market(1); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketBetter.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.Rating = -1.0; + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); + marketBetter.SetPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketBetter); // when - SetRating(marketBetter, 1.0); + SetRating(marketBetter, 2.0); // then _marketCacheResults.Data.Count.Should().Be(2); + _marketCacheResults.Summary.Overall.Refreshes.Should().Be(1); results.Data.Count.Should().Be(PricesPerMarket); results.Summary.Overall.Adds.Should().Be(PricesPerMarket); - results.Summary.Overall.Updates.Should().Be(PricesPerMarket); - results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); + results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); resultsLow.Data.Count.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); - resultsLow.Summary.Overall.Updates.Should().Be(0); - resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); + resultsLow.Summary.Overall.Updates.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Removes.Should().Be(0); + resultsLow.Summary.Overall.Refreshes.Should().Be(0); + resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + resultsRefresh.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsRefresh.Summary.Overall.Updates.Should().Be(PricesPerMarket); + resultsRefresh.Summary.Overall.Removes.Should().Be(0); + resultsRefresh.Summary.Overall.Refreshes.Should().Be(0); + resultsRefresh.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketBetter.Id)); + resultsLowRefresh.Data.Count.Should().Be(PricesPerMarket); + resultsLowRefresh.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsLowRefresh.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); + resultsLowRefresh.Summary.Overall.Removes.Should().Be(0); + resultsLowRefresh.Summary.Overall.Refreshes.Should().Be(0); + resultsLowRefresh.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); } [Fact] @@ -582,15 +621,15 @@ public void ChildComparerUpdatesToCorrectValueOnUpdate() { // having using var resultsLow = ChangeSetByLowRating(false).AsAggregator(); - using var resultsLowPrice = ChangeSetByRatingThenLow(false).AsAggregator(); - using var resultsHighPrice = ChangeSetByRatingThenHigh(false).AsAggregator(); + using var resultsLowPrice = ChangeSetByRatingThenLowPrice(false).AsAggregator(); + using var resultsHighPrice = ChangeSetByRatingThenHighPrice(false).AsAggregator(); var marketOriginal = new Market(0); var marketHighest = new Market(1); var marketLowest = new Market(2); marketLowest.Rating = marketHighest.Rating = 1.0; - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); marketHighest.SetPrices(0, PricesPerMarket, HighestPrice); - marketLowest.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); + marketLowest.SetPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketHighest); _marketCache.AddOrUpdate(marketLowest); @@ -603,84 +642,112 @@ public void ChildComparerUpdatesToCorrectValueOnUpdate() resultsLow.Data.Count.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Summary.Overall.Removes.Should().Be(0); + resultsLow.Summary.Overall.Refreshes.Should().Be(0); resultsLow.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); resultsLowPrice.Data.Count.Should().Be(PricesPerMarket); resultsLowPrice.Summary.Overall.Adds.Should().Be(PricesPerMarket); - resultsLowPrice.Summary.Overall.Updates.Should().Be(PricesPerMarket); + resultsLowPrice.Summary.Overall.Updates.Should().Be(PricesPerMarket * 3); + resultsLowPrice.Summary.Overall.Removes.Should().Be(0); + resultsLowPrice.Summary.Overall.Refreshes.Should().Be(0); resultsLowPrice.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketLowest.Id)); resultsHighPrice.Data.Count.Should().Be(PricesPerMarket); resultsHighPrice.Summary.Overall.Adds.Should().Be(PricesPerMarket); - resultsHighPrice.Summary.Overall.Updates.Should().Be(0); + resultsHighPrice.Summary.Overall.Updates.Should().Be(PricesPerMarket); + resultsHighPrice.Summary.Overall.Removes.Should().Be(0); + resultsHighPrice.Summary.Overall.Refreshes.Should().Be(0); resultsHighPrice.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketHighest.Id)); } -#if false [Fact] - public void ComparerOnlyUpdatesVisibleValuesOnUpdate() + public void ChildComparerOnlyUpdatesVisibleValuesOnUpdate() { // having - using var highPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.HighPriceCompare).AsAggregator(); - using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.LowPriceCompare).AsAggregator(); + using var results = ChangeSetByRating(false).AsAggregator(); + using var lowRatingLowPriceResults = ChangeSetByLowRatingThenLowPrice(false).AsAggregator(); + using var lowRatingHighPriceResults = ChangeSetByLowRatingThenHighPrice(false).AsAggregator(); var marketOriginal = new Market(0); var marketLow = new Market(1); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); + var marketLowest = new Market(2); + + marketLowest.Rating = marketLow.Rating = -1; + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); + marketLow.SetPrices(0, PricesPerMarket, LowestPrice); + marketLowest.SetPrices(0, PricesPerMarket, GetRandomPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); + _marketCache.AddOrUpdate(marketLowest); // when - marketLow.UpdateAllPrices(LowestPrice - 1); + marketLowest.UpdateAllPrices(LowestPrice - 1); // then - _marketCacheResults.Data.Count.Should().Be(2); - lowPriceResults.Data.Count.Should().Be(PricesPerMarket); - lowPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); - lowPriceResults.Summary.Overall.Removes.Should().Be(0); - lowPriceResults.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); - lowPriceResults.Summary.Overall.Refreshes.Should().Be(0); - lowPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketLow.Id)); - highPriceResults.Data.Count.Should().Be(PricesPerMarket); - highPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); - highPriceResults.Summary.Overall.Removes.Should().Be(0); - highPriceResults.Summary.Overall.Updates.Should().Be(0); - highPriceResults.Summary.Overall.Refreshes.Should().Be(0); - highPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); + _marketCacheResults.Data.Count.Should().Be(3); + results.Data.Count.Should().Be(PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); + results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); + lowRatingLowPriceResults.Data.Count.Should().Be(PricesPerMarket); + lowRatingLowPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); + lowRatingLowPriceResults.Summary.Overall.Removes.Should().Be(0); + lowRatingLowPriceResults.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); + lowRatingLowPriceResults.Summary.Overall.Refreshes.Should().Be(0); + lowRatingLowPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketLowest.Id)); + lowRatingHighPriceResults.Data.Count.Should().Be(PricesPerMarket); + lowRatingHighPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); + lowRatingHighPriceResults.Summary.Overall.Removes.Should().Be(0); + lowRatingHighPriceResults.Summary.Overall.Updates.Should().Be(PricesPerMarket * 3); + lowRatingHighPriceResults.Summary.Overall.Refreshes.Should().Be(0); + lowRatingHighPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketLow.Id)); } [Fact] - public void ComparerOnlyRefreshesVisibleValues() + public void ChildComparerOnlyRefreshesVisibleValues() { // having - using var highPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer, MarketPrice.HighPriceCompare).AsAggregator(); - using var lowPriceResults = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer, MarketPrice.LowPriceCompare).AsAggregator(); + using var results = ChangeSetByRating(false).AsAggregator(); + using var lowRatingLowPriceResults = ChangeSetByLowRatingThenLowPrice(false).AsAggregator(); + using var lowRatingHighPriceResults = ChangeSetByLowRatingThenHighPrice(false).AsAggregator(); var marketOriginal = new Market(0); var marketLow = new Market(1); - marketOriginal.AddRandomPrices(0, PricesPerMarket, GetRandomPrice); - marketLow.UpdatePrices(0, PricesPerMarket, LowestPrice); + var marketLowest = new Market(2); + + marketLowest.Rating = marketLow.Rating = -1; + marketOriginal.SetPrices(0, PricesPerMarket, GetRandomPrice); + marketLow.SetPrices(0, PricesPerMarket, GetRandomPrice); + marketLowest.SetPrices(0, PricesPerMarket, LowestPrice); _marketCache.AddOrUpdate(marketOriginal); _marketCache.AddOrUpdate(marketLow); + _marketCache.AddOrUpdate(marketLowest); // when - marketLow.RefreshAllPrices(LowestPrice - 1); + marketLowest.RefreshAllPrices(LowestPrice - 1); // then - _marketCacheResults.Data.Count.Should().Be(2); - lowPriceResults.Data.Count.Should().Be(PricesPerMarket); - lowPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); - lowPriceResults.Summary.Overall.Removes.Should().Be(0); - lowPriceResults.Summary.Overall.Updates.Should().Be(PricesPerMarket); - lowPriceResults.Summary.Overall.Refreshes.Should().Be(PricesPerMarket); - lowPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketLow.Id)); - highPriceResults.Data.Count.Should().Be(PricesPerMarket); - highPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); - highPriceResults.Summary.Overall.Removes.Should().Be(0); - highPriceResults.Summary.Overall.Updates.Should().Be(0); - highPriceResults.Summary.Overall.Refreshes.Should().Be(0); - highPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); + _marketCacheResults.Data.Count.Should().Be(3); + results.Data.Count.Should().Be(PricesPerMarket); + results.Summary.Overall.Adds.Should().Be(PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(0); + results.Summary.Overall.Updates.Should().Be(0); + results.Summary.Overall.Refreshes.Should().Be(0); + results.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketOriginal.Id)); + lowRatingLowPriceResults.Data.Count.Should().Be(PricesPerMarket); + lowRatingLowPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); + lowRatingLowPriceResults.Summary.Overall.Removes.Should().Be(0); + lowRatingLowPriceResults.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); + lowRatingLowPriceResults.Summary.Overall.Refreshes.Should().Be(PricesPerMarket); + lowRatingLowPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketLowest.Id)); + lowRatingHighPriceResults.Data.Count.Should().Be(PricesPerMarket); + lowRatingHighPriceResults.Summary.Overall.Adds.Should().Be(PricesPerMarket); + lowRatingHighPriceResults.Summary.Overall.Removes.Should().Be(0); + lowRatingHighPriceResults.Summary.Overall.Updates.Should().Be(PricesPerMarket); + lowRatingHighPriceResults.Summary.Overall.Refreshes.Should().Be(0); + lowRatingHighPriceResults.Data.Items.Select(cp => cp.MarketId).ForEach(guid => guid.Should().Be(marketLow.Id)); } -#endif [Fact] public void EqualityComparerHidesUpdatesWithoutChanges() @@ -704,6 +771,131 @@ public void EqualityComparerHidesUpdatesWithoutChanges() results.Summary.Overall.Refreshes.Should().Be(0); } + [Fact] + public void EqualityComparerAndChildComparerWorkTogetherForUpdates() + { + // having + using var resultsLow = ChangeSetByLowRating().AsAggregator(); + using var resultsRecent = ChangeSetByRatingThenRecent().AsAggregator(); + using var resultsTimeStamp = ChangeSetByRatingThenTimeStamp().AsAggregator(); + var marketLow = new Market(0); + var market = new Market(1); + marketLow.Rating = -1; + marketLow.SetPrices(0, PricesPerMarket, GetRandomPrice); + market.SetPrices(0, PricesPerMarket, GetRandomPrice); + _marketCache.AddOrUpdate(marketLow); + _marketCache.AddOrUpdate(market); + market.SetPrices(0, PricesPerMarket, LowestPrice); + + // when + market.UpdateAllPrices(LowestPrice); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + resultsLow.Data.Count.Should().Be(PricesPerMarket); + resultsLow.Messages.Count.Should().Be(1); + resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Removes.Should().Be(0); + resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Summary.Overall.Refreshes.Should().Be(0); + resultsRecent.Messages.Count.Should().Be(3); + resultsRecent.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsRecent.Summary.Overall.Removes.Should().Be(0); + resultsRecent.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); + resultsRecent.Summary.Overall.Refreshes.Should().Be(0); + resultsTimeStamp.Messages.Count.Should().Be(4); + resultsTimeStamp.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsTimeStamp.Summary.Overall.Removes.Should().Be(0); + resultsTimeStamp.Summary.Overall.Updates.Should().Be(PricesPerMarket * 3); + resultsTimeStamp.Summary.Overall.Refreshes.Should().Be(0); + } + + [Fact] + public void EqualityComparerAndChildComparerWorkTogetherForRefreshes() + { + // having + using var resultsLow = ChangeSetByLowRating().AsAggregator(); + using var resultsRecent = ChangeSetByRatingThenRecent().AsAggregator(); + using var resultsTimeStamp = ChangeSetByRatingThenTimeStamp().AsAggregator(); + var marketLow = new Market(0); + var market = new Market(1); + marketLow.Rating = -1; + marketLow.SetPrices(0, PricesPerMarket, GetRandomPrice); + market.SetPrices(0, PricesPerMarket, GetRandomPrice); + _marketCache.AddOrUpdate(marketLow); + _marketCache.AddOrUpdate(market); + market.SetPrices(0, PricesPerMarket, LowestPrice); + // Update again, but only the timestamp will change, so resultsRecent will ignore + market.SetPrices(0, PricesPerMarket, LowestPrice); + + // when + // resultsRecent won't see the refresh because it ignored the update + // resultsTimeStamp will see the refreshes because it didn't + market.RefreshAllPrices(LowestPrice); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + resultsLow.Data.Count.Should().Be(PricesPerMarket); + resultsLow.Messages.Count.Should().Be(1); + resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Removes.Should().Be(0); + resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Summary.Overall.Refreshes.Should().Be(0); + resultsRecent.Messages.Count.Should().Be(3); + resultsRecent.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsRecent.Summary.Overall.Removes.Should().Be(0); + resultsRecent.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2); + resultsRecent.Summary.Overall.Refreshes.Should().Be(0); + resultsTimeStamp.Messages.Count.Should().Be(5); + resultsTimeStamp.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsTimeStamp.Summary.Overall.Removes.Should().Be(0); + resultsTimeStamp.Summary.Overall.Updates.Should().Be(PricesPerMarket * 3); + resultsTimeStamp.Summary.Overall.Refreshes.Should().Be(PricesPerMarket); + } + + [Fact] + public void EqualityComparerAndChildComparerRefreshesBecomeUpdates() + { + // having + using var resultsLow = ChangeSetByLowRating().AsAggregator(); + using var resultsRecent = ChangeSetByRatingThenRecent().AsAggregator(); + using var resultsTimeStamp = ChangeSetByRatingThenTimeStamp().AsAggregator(); + var marketLow = new Market(0); + var market = new Market(1); + marketLow.Rating = -1; + marketLow.SetPrices(0, PricesPerMarket, GetRandomPrice); + market.SetPrices(0, PricesPerMarket, GetRandomPrice); + _marketCache.AddOrUpdate(marketLow); + _marketCache.AddOrUpdate(market); + market.SetPrices(0, PricesPerMarket, LowestPrice); + // Update again, but only the timestamp will change, so resultsRecent will ignore + market.SetPrices(0, PricesPerMarket, LowestPrice); + + // when + // resultsRecent won't see the refresh because it ignored the update + // resultsTimeStamp will see the refreshes because it didn't + market.RefreshAllPrices(GetRandomPrice); + + // then + _marketCacheResults.Data.Count.Should().Be(2); + resultsLow.Data.Count.Should().Be(PricesPerMarket); + resultsLow.Messages.Count.Should().Be(1); + resultsLow.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsLow.Summary.Overall.Removes.Should().Be(0); + resultsLow.Summary.Overall.Updates.Should().Be(0); + resultsLow.Summary.Overall.Refreshes.Should().Be(0); + resultsRecent.Messages.Count.Should().Be(4); + resultsRecent.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsRecent.Summary.Overall.Removes.Should().Be(0); + resultsRecent.Summary.Overall.Updates.Should().Be(PricesPerMarket * 3); + resultsRecent.Summary.Overall.Refreshes.Should().Be(0); + resultsTimeStamp.Messages.Count.Should().Be(5); + resultsTimeStamp.Summary.Overall.Adds.Should().Be(PricesPerMarket); + resultsTimeStamp.Summary.Overall.Removes.Should().Be(0); + resultsTimeStamp.Summary.Overall.Updates.Should().Be(PricesPerMarket * 3); + resultsTimeStamp.Summary.Overall.Refreshes.Should().Be(PricesPerMarket); + } + [Fact] public void EveryItemVisibleWhenSequenceCompletes() { @@ -711,7 +903,7 @@ public void EveryItemVisibleWhenSequenceCompletes() _marketCache.AddOrUpdate(Enumerable.Range(0, MarketCount).Select(n => new FixedMarket(GetRandomPrice, n * ItemIdStride, (n * ItemIdStride) + PricesPerMarket))); // when - using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, Market.RatingCompare).AsAggregator(); + using var results = ChangeSetByRating(false).AsAggregator(); DisposeMarkets(); // then @@ -775,11 +967,13 @@ private IObservable> CreateChangeSet(string name, I .DebugSpy($"{name} [Results]"); private IObservable> ChangeSetByRating(bool resortOnRefresh = true) => CreateChangeSet("Rating", resortOnRefresh: resortOnRefresh); - private IObservable> ChangeSetByLowRating(bool resortOnRefresh = true) => CreateChangeSet("Low Rating", Market.RatingCompare.Invert(), resortOnRefresh: resortOnRefresh); - private IObservable> ChangeSetByRatingThenHigh(bool resortOnRefresh = true) => CreateChangeSet("Rating | High", Market.RatingCompare, MarketPrice.HighPriceCompare, resortOnRefresh: resortOnRefresh); - private IObservable> ChangeSetByRatingThenLow(bool resortOnRefresh = true) => CreateChangeSet("Rating | Low", Market.RatingCompare, MarketPrice.LowPriceCompare, resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByRatingThenHighPrice(bool resortOnRefresh = true) => CreateChangeSet("Rating | High", Market.RatingCompare, MarketPrice.HighPriceCompare, resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByRatingThenLowPrice(bool resortOnRefresh = true) => CreateChangeSet("Rating | Low", Market.RatingCompare, MarketPrice.LowPriceCompare, resortOnRefresh: resortOnRefresh); private IObservable> ChangeSetByRatingThenRecent(bool resortOnRefresh = true) => CreateChangeSet("Rating | Recent", Market.RatingCompare, MarketPrice.LatestPriceCompare, equalityComparer: MarketPrice.EqualityComparer, resortOnRefresh: resortOnRefresh); - private IObservable> ChangeSetByRatingThenTimestamp(bool resortOnRefresh = true) => CreateChangeSet("Rating | Timestamp", Market.RatingCompare, MarketPrice.LatestPriceCompare, equalityComparer: MarketPrice.EqualityComparerWithTimeStamp, resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByRatingThenTimeStamp(bool resortOnRefresh = true) => CreateChangeSet("Rating | Timestamp", Market.RatingCompare, MarketPrice.LatestPriceCompare, equalityComparer: MarketPrice.EqualityComparerWithTimeStamp, resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByLowRating(bool resortOnRefresh = true) => CreateChangeSet("Low Rating", Market.RatingCompare.Invert(), resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByLowRatingThenHighPrice(bool resortOnRefresh = true) => CreateChangeSet("Low Rating | High", Market.RatingCompare.Invert(), MarketPrice.HighPriceCompare, resortOnRefresh: resortOnRefresh); + private IObservable> ChangeSetByLowRatingThenLowPrice(bool resortOnRefresh = true) => CreateChangeSet("Low Rating | Low", Market.RatingCompare.Invert(), MarketPrice.LowPriceCompare, resortOnRefresh: resortOnRefresh); private IMarket SetRating(IMarket market, double newRating) { diff --git a/src/DynamicData.Tests/Domain/Market.cs b/src/DynamicData.Tests/Domain/Market.cs index f28673a9c..1e8b25c47 100644 --- a/src/DynamicData.Tests/Domain/Market.cs +++ b/src/DynamicData.Tests/Domain/Market.cs @@ -58,44 +58,43 @@ public Market AddRandomIdPrices(Random r, int count, int minId, int maxId, Func< return this; } - public Market AddRandomPrices(int minId, int maxId, Func randPrices) - { - _latestPrices.AddOrUpdate(Enumerable.Range(minId, maxId - minId).Select(id => CreatePrice(id, randPrices()))); - return this; - } + public Market AddUniquePrices(int section, int count, int stride, Func getPrice) => SetPrices(section * stride, section * stride + count, getPrice); - public Market AddUniquePrices(int section, int count, int stride, Func randPrices) => AddRandomPrices(section * stride, section * stride + count, randPrices); - - public Market RefreshAllPrices(decimal newPrice) - { - _latestPrices.Edit(updater => updater.Items.ForEach(cp => + public Market RefreshPrice(int id, decimal newPrice) => this.With(_ => + _latestPrices.Edit(updater => updater.Lookup(id).IfHasValue(cp => { cp.Price = newPrice; updater.Refresh(cp); - })); - - return this; - } - - public Market RefreshAllPrices(Func randPrices) => RefreshAllPrices(randPrices()); + }))); - public Market RefreshPrice(int id, decimal newPrice) - { - _latestPrices.Edit(updater => updater.Lookup(id).IfHasValue(cp => + public Market RefreshAllPrices(Func getNewPrice) => this.With(_ => + _latestPrices.Edit(updater => updater.Items.ForEach(cp => { - cp.Price = newPrice; + cp.Price = getNewPrice(cp.ItemId); updater.Refresh(cp); - })); - return this; - } + }))); + + public Market RefreshAllPrices(Func getNewPrice) => RefreshAllPrices(_ => getNewPrice()); + + public Market RefreshAllPrices(decimal newPrice) => RefreshAllPrices(_ => newPrice); public void RemoveAllPrices() => this.With(_ => _latestPrices.Clear()); public void RemovePrice(int itemId) => this.With(_ => _latestPrices.Remove(itemId)); - public Market UpdateAllPrices(decimal newPrice) => this.With(_ => _latestPrices.Edit(updater => updater.AddOrUpdate(updater.Items.Select(cp => CreatePrice(cp.ItemId, newPrice))))); + public Market UpdateAllPrices(Func getNewPrice) => this.With(_ => + _latestPrices.Edit(updater => updater.AddOrUpdate(updater.Items.Select(cp => CreatePrice(cp.ItemId, getNewPrice(cp.ItemId)))))); + + public Market UpdateAllPrices(Func getNewPrice) => UpdateAllPrices(_ => getNewPrice()); + + public Market UpdateAllPrices(decimal newPrice) => UpdateAllPrices(_ => newPrice); + + public Market SetPrices(int minId, int maxId, Func getPrice) => this.With(_ => + _latestPrices.AddOrUpdate(Enumerable.Range(minId, maxId - minId).Select(id => CreatePrice(id, getPrice(id))))); + + public Market SetPrices(int minId, int maxId, Func getPrice) => SetPrices(minId, maxId, i => getPrice()); - public Market SetPrices(int minId, int maxId, decimal newPrice) => this.With(_ => _latestPrices.AddOrUpdate(Enumerable.Range(minId, maxId - minId).Select(id => CreatePrice(id, newPrice)))); + public Market SetPrices(int minId, int maxId, decimal newPrice) => SetPrices(minId, maxId, _ => newPrice); public void Dispose() => _latestPrices.Dispose(); diff --git a/src/DynamicData/Cache/ObservableCacheEx.cs b/src/DynamicData/Cache/ObservableCacheEx.cs index 8990c8f4b..11cdb311b 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.cs @@ -27,7 +27,7 @@ namespace DynamicData; public static class ObservableCacheEx { private const int DefaultSortResetThreshold = 100; - private const bool DefaultResortOnSourceRefresh = false; + private const bool DefaultResortOnSourceRefresh = true; /// /// Inject side effects into the stream using the specified adaptor.