Skip to content

Commit d0bbbed

Browse files
authored
test: Additional tests for the extension methods. (#59)
1 parent b6a2c3b commit d0bbbed

File tree

9 files changed

+915
-96
lines changed

9 files changed

+915
-96
lines changed

src/CommunityToolkit.Datasync.Client/CommunityToolkit.Datasync.Client.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
<ItemGroup>
77
<InternalsVisibleTo Include="CommunityToolkit.Datasync.Client.Test" />
8+
<InternalsVisibleTo Include="DynamicProxyGenAssembly2"/>
89
</ItemGroup>
910

1011
<ItemGroup>

src/CommunityToolkit.Datasync.Client/Exceptions/EntityDoesNotExistException.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ public class EntityDoesNotExistException(ServiceResponse serviceResponse, Uri en
2020
/// <summary>
2121
/// The endpoint of the table that was used for the entity.
2222
/// </summary>
23-
public Uri Endpoint { get; set; } = endpoint;
23+
public Uri Endpoint { get; } = endpoint;
2424

2525
/// <summary>
2626
/// The ID of the entity that is missing.
2727
/// </summary>
28-
public string Id { get; set; } = id;
28+
public string Id { get; } = id;
2929
}

src/CommunityToolkit.Datasync.Client/Paging/ConcurrentObservableCollection.cs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using CommunityToolkit.Datasync.Client.Paging;
56
using System.Collections.ObjectModel;
67
using System.Collections.Specialized;
78
using System.ComponentModel;
@@ -151,34 +152,38 @@ public bool ReplaceIf(Func<T, bool> match, T replacement)
151152
/// </summary>
152153
/// <param name="e">The event arguments</param>
153154
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
154-
{
155-
if (SynchronizationContext.Current == this.context)
156-
{
157-
RaiseCollectionChanged(e);
158-
}
159-
else
160-
{
161-
this.context.Send(RaiseCollectionChanged, e);
162-
}
163-
}
155+
=> DispatchCallback(new SynchronizationContextAdapter(this.context), RaiseCollectionChanged, e);
164156

165157
/// <summary>
166158
/// Event trigger to indicate that a property has changed in a thread-safe way.
167159
/// </summary>
168160
/// <param name="e">The event arguments</param>
169161
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
162+
=> DispatchCallback(new SynchronizationContextAdapter(this.context), RaisePropertyChanged, e);
163+
164+
/// <summary>
165+
/// Dispatches the callback to the synchronization context. If the synchronization context is
166+
/// the current one, we can avoid a dispatch and just call the callback directly.
167+
/// </summary>
168+
/// <param name="context">The context to send the request to.</param>
169+
/// <param name="callback">The callback method.</param>
170+
/// <param name="param">The parameter for the callback method.</param>
171+
internal static void DispatchCallback(ISynchronizationContext context, SendOrPostCallback callback, object? param)
170172
{
171-
if (SynchronizationContext.Current == this.context)
173+
if (context.IsCurrentContext())
172174
{
173-
RaisePropertyChanged(e);
175+
callback(param);
174176
}
175177
else
176178
{
177-
this.context.Send(RaisePropertyChanged, e);
179+
context.Send(callback, param);
178180
}
179181
}
180182

181-
[ExcludeFromCodeCoverage]
183+
/// <summary>
184+
/// Raises the <see cref="OnCollectionChanged(NotifyCollectionChangedEventArgs)"/> event on this collection.
185+
/// </summary>
186+
/// <param name="param"></param>
182187
private void RaiseCollectionChanged(object? param)
183188
{
184189
if (!this.suppressNotification)
@@ -187,7 +192,10 @@ private void RaiseCollectionChanged(object? param)
187192
}
188193
}
189194

190-
[ExcludeFromCodeCoverage]
195+
/// <summary>
196+
/// Raises the <see cref="OnPropertyChanged(PropertyChangedEventArgs)"/> event on this collection.
197+
/// </summary>
198+
/// <param name="param"></param>
191199
private void RaisePropertyChanged(object? param)
192200
{
193201
base.OnPropertyChanged((PropertyChangedEventArgs)param!);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace CommunityToolkit.Datasync.Client.Paging;
8+
9+
/// <summary>
10+
/// An abstraction layer for the <see cref="SynchronizationContext"/> that we use
11+
/// for mocking out context calls.
12+
/// </summary>
13+
internal interface ISynchronizationContext
14+
{
15+
bool IsCurrentContext();
16+
void Send(SendOrPostCallback callback, object? state);
17+
}
18+
19+
/// <summary>
20+
/// A concrete implementation of the <see cref="ISynchronizationContext"/> that handles
21+
/// a real synchronization context.
22+
/// </summary>
23+
[ExcludeFromCodeCoverage]
24+
internal class SynchronizationContextAdapter(SynchronizationContext context) : ISynchronizationContext
25+
{
26+
public bool IsCurrentContext()
27+
=> SynchronizationContext.Current == context;
28+
29+
public void Send(SendOrPostCallback callback, object? state)
30+
=> context.Send(callback, state);
31+
}

src/CommunityToolkit.Datasync.Client/Query/IAsyncEnumerableExtensions.cs

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -72,40 +72,6 @@ internal static async ValueTask<Dictionary<TKey, TSource>> ToDatasyncDictionaryA
7272
return d;
7373
}
7474

75-
/// <summary>
76-
/// Converts an async-enumerable sequence to an enumerable sequence.
77-
/// </summary>
78-
/// <typeparam name="TSource">The type of the elements in the source sequence.</typeparam>
79-
/// <param name="source">An async-enumerable sequence to convert to an enumerable sequence.</param>
80-
/// <returns>The enumerable sequence containing the elements in the async-enumerable sequence.</returns>
81-
/// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
82-
internal static IEnumerable<TSource> ToDatasyncEnumerable<TSource>(this IAsyncEnumerable<TSource> source)
83-
{
84-
ArgumentNullException.ThrowIfNull(source, nameof(source));
85-
return Core(source);
86-
87-
static IEnumerable<TSource> Core(IAsyncEnumerable<TSource> source)
88-
{
89-
IAsyncEnumerator<TSource> e = source.GetAsyncEnumerator(default);
90-
try
91-
{
92-
while (true)
93-
{
94-
if (!DatasyncWait(e.MoveNextAsync()))
95-
{
96-
break;
97-
}
98-
99-
yield return e.Current;
100-
}
101-
}
102-
finally
103-
{
104-
DatasyncWait(e.DisposeAsync());
105-
}
106-
}
107-
}
108-
10975
/// <summary>
11076
/// Creates a hash set from an async-enumerable sequence.
11177
/// </summary>
@@ -187,32 +153,5 @@ internal static async ValueTask<ConcurrentObservableCollection<TSource>> ToDatas
187153
existingCollection.ReplaceAll(list);
188154
return existingCollection;
189155
}
190-
191-
// NB: ValueTask and ValueTask<T> do not have to support blocking on a call to GetResult when backed by
192-
// an IValueTaskSource or IValueTaskSource<T> implementation. Convert to a Task or Task<T> to do so
193-
// in case the task hasn't completed yet.
194-
195-
private static void DatasyncWait(ValueTask task)
196-
{
197-
ValueTaskAwaiter awaiter = task.GetAwaiter();
198-
if (!awaiter.IsCompleted)
199-
{
200-
task.AsTask().GetAwaiter().GetResult();
201-
return;
202-
}
203-
204-
awaiter.GetResult();
205-
}
206-
207-
private static T DatasyncWait<T>(ValueTask<T> task)
208-
{
209-
ValueTaskAwaiter<T> awaiter = task.GetAwaiter();
210-
if (!awaiter.IsCompleted)
211-
{
212-
return task.AsTask().GetAwaiter().GetResult();
213-
}
214-
215-
return awaiter.GetResult();
216-
}
217156
}
218157

src/CommunityToolkit.Datasync.Client/Query/IDatasyncQueryableExtensions.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public static class IDatasyncQueryableExtensions
1818
/// <returns>A task that represents the asynchronous operation. The task result contains the number of elements that would be returned by the query.</returns>
1919
public static async ValueTask<int> CountAsync<TSource>(this IDatasyncQueryable<TSource> query, CancellationToken cancellationToken = default) where TSource : class
2020
{
21-
ArgumentNullException.ThrowIfNull(query, nameof(query));
2221
ServiceResponse<int> response = await query.ServiceClient.CountAsync(query, new DatasyncServiceOptions(), cancellationToken).ConfigureAwait(false);
2322
return response.Value;
2423
}
@@ -32,7 +31,6 @@ public static async ValueTask<int> CountAsync<TSource>(this IDatasyncQueryable<T
3231
/// <returns>A task that represents the asynchronous operation. The task result contains the number of elements that would be returned by the query.</returns>
3332
public static async ValueTask<long> LongCountAsync<TSource>(this IDatasyncQueryable<TSource> query, CancellationToken cancellationToken = default) where TSource : class
3433
{
35-
ArgumentNullException.ThrowIfNull(query, nameof(query));
3634
ServiceResponse<long> response = await query.ServiceClient.LongCountAsync(query, new DatasyncServiceOptions(), cancellationToken ).ConfigureAwait(false);
3735
return response.Value;
3836
}
@@ -54,10 +52,7 @@ public static ValueTask<TSource[]> ToArrayAsync<TSource>(this IDatasyncQueryable
5452
/// <param name="query">The query to execute on the remote service..</param>
5553
/// <returns>The async-enumerable sequence whose elements are pulled from the result set.</returns>
5654
public static IAsyncEnumerable<TSource> ToAsyncEnumerable<TSource>(this IDatasyncQueryable<TSource> query) where TSource : class
57-
{
58-
ArgumentNullException.ThrowIfNull(query, nameof(query));
59-
return query.ServiceClient.Query(query);
60-
}
55+
=> query.ServiceClient.Query(query);
6156

6257
/// <summary>
6358
/// Executes a query on the remote service, returning the results as an async-enumerable sequence.
@@ -136,7 +131,7 @@ public static ValueTask<List<TSource>> ToListAsync<TSource>(this IDatasyncQuerya
136131
/// <param name="query">The source table query.</param>
137132
/// <param name="cancellationToken">The optional cancellation token to be used for cancelling the sequence at any time.</param>
138133
/// <returns>A <see cref="ConcurrentObservableCollection{T}"/> containing all the elements of the source sequence.</returns>
139-
public static ValueTask<ConcurrentObservableCollection<TSource>> ToObservableCollection<TSource>(this IDatasyncQueryable<TSource> query, CancellationToken cancellationToken = default) where TSource : class
134+
public static ValueTask<ConcurrentObservableCollection<TSource>> ToObservableCollectionAsync<TSource>(this IDatasyncQueryable<TSource> query, CancellationToken cancellationToken = default) where TSource : class
140135
=> query.ToAsyncEnumerable().ToDatasyncObservableCollectionAsync(cancellationToken);
141136

142137
/// <summary>
@@ -147,6 +142,6 @@ public static ValueTask<ConcurrentObservableCollection<TSource>> ToObservableCol
147142
/// <param name="collection">The <see cref="ConcurrentObservableCollection{T}"/> to update.</param>
148143
/// <param name="cancellationToken">The optional cancellation token to be used for cancelling the sequence at any time.</param>
149144
/// <returns>The <see cref="ConcurrentObservableCollection{T}"/> passed in containing all the elements of the source sequence (replacing the old content).</returns>
150-
public static ValueTask<ConcurrentObservableCollection<TSource>> ToObservableCollection<TSource>(this IDatasyncQueryable<TSource> query, ConcurrentObservableCollection<TSource> collection, CancellationToken cancellationToken = default) where TSource : class
145+
public static ValueTask<ConcurrentObservableCollection<TSource>> ToObservableCollectionAsync<TSource>(this IDatasyncQueryable<TSource> query, ConcurrentObservableCollection<TSource> collection, CancellationToken cancellationToken = default) where TSource : class
151146
=> query.ToAsyncEnumerable().ToDatasyncObservableCollectionAsync(collection, cancellationToken);
152147
}

src/CommunityToolkit.Datasync.Client/Service/IDatasyncServiceClientExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public static ValueTask<List<TSource>> ToListAsync<TSource>(this IReadOnlyDatasy
154154
/// <param name="source">The source service client.</param>
155155
/// <param name="cancellationToken">The optional cancellation token to be used for cancelling the sequence at any time.</param>
156156
/// <returns>A <see cref="ConcurrentObservableCollection{T}"/> containing all the elements of the source sequence.</returns>
157-
public static ValueTask<ConcurrentObservableCollection<TSource>> ToObservableCollection<TSource>(this IReadOnlyDatasyncServiceClient<TSource> source, CancellationToken cancellationToken = default) where TSource : class
157+
public static ValueTask<ConcurrentObservableCollection<TSource>> ToObservableCollectionAsync<TSource>(this IReadOnlyDatasyncServiceClient<TSource> source, CancellationToken cancellationToken = default) where TSource : class
158158
=> source.ToAsyncEnumerable().ToDatasyncObservableCollectionAsync(cancellationToken);
159159

160160
/// <summary>
@@ -165,7 +165,7 @@ public static ValueTask<ConcurrentObservableCollection<TSource>> ToObservableCol
165165
/// <param name="collection">The <see cref="ConcurrentObservableCollection{T}"/> to update.</param>
166166
/// <param name="cancellationToken">The optional cancellation token to be used for cancelling the sequence at any time.</param>
167167
/// <returns>The <see cref="ConcurrentObservableCollection{T}"/> passed in containing all the elements of the source sequence (replacing the old content).</returns>
168-
public static ValueTask<ConcurrentObservableCollection<TSource>> ToObservableCollection<TSource>(this IReadOnlyDatasyncServiceClient<TSource> source, ConcurrentObservableCollection<TSource> collection, CancellationToken cancellationToken = default) where TSource : class
168+
public static ValueTask<ConcurrentObservableCollection<TSource>> ToObservableCollectionAsync<TSource>(this IReadOnlyDatasyncServiceClient<TSource> source, ConcurrentObservableCollection<TSource> collection, CancellationToken cancellationToken = default) where TSource : class
169169
=> source.ToAsyncEnumerable().ToDatasyncObservableCollectionAsync(collection, cancellationToken);
170170

171171
/// <summary>

tests/CommunityToolkit.Datasync.Client.Test/Paging/ConcurrentObservableCollection_Tests.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using CommunityToolkit.Datasync.TestCommon.Databases;
5+
#pragma warning disable IDE0028 // Simplify collection initialization
6+
#pragma warning disable IDE0305 // Simplify collection initialization
67

8+
using CommunityToolkit.Datasync.Client.Paging;
9+
using CommunityToolkit.Datasync.TestCommon.Databases;
10+
using NSubstitute;
711
using TestData = CommunityToolkit.Datasync.TestCommon.TestData;
812

913
namespace CommunityToolkit.Datasync.Client.Test.Paging;
@@ -21,6 +25,13 @@ public ConcurrentObservableCollection_Tests()
2125
this.movies.CollectionChanged += (sender, e) => this.collectionChangedCallCount++;
2226
}
2327

28+
[Fact]
29+
public void DefaultCtor_Creates_EmptyCollection()
30+
{
31+
ConcurrentObservableCollection<InMemoryMovie> sut = new();
32+
sut.Should().BeEmpty();
33+
}
34+
2435
[Fact]
2536
public void ReplaceAll_Null_Throws()
2637
{
@@ -169,4 +180,40 @@ public void ReplaceIf_False_IfNoMatch()
169180
actual.Should().BeFalse();
170181
this.movies.Should().HaveCount(count).And.NotContain(item);
171182
}
183+
184+
[Fact]
185+
public void DispatchCallback_DispatchesToSynchronizationContext()
186+
{
187+
int contextCaller = 0;
188+
int functionCaller = 0;
189+
190+
void dispatcher(object p) { functionCaller++; }
191+
192+
ISynchronizationContext context = Substitute.For<ISynchronizationContext>();
193+
context.IsCurrentContext().Returns(false);
194+
context.When(x => x.Send(Arg.Any<SendOrPostCallback>(), Arg.Any<object>())).Do(x => contextCaller++);
195+
196+
ConcurrentObservableCollection<InMemoryMovie>.DispatchCallback(context, dispatcher, new object());
197+
198+
contextCaller.Should().Be(1);
199+
functionCaller.Should().Be(0);
200+
}
201+
202+
[Fact]
203+
public void DispatchCallback_DispatchesLocally()
204+
{
205+
int contextCaller = 0;
206+
int functionCaller = 0;
207+
208+
void dispatcher(object p) { functionCaller++; }
209+
210+
ISynchronizationContext context = Substitute.For<ISynchronizationContext>();
211+
context.IsCurrentContext().Returns(true);
212+
context.When(x => x.Send(Arg.Any<SendOrPostCallback>(), Arg.Any<object>())).Do(x => contextCaller++);
213+
214+
ConcurrentObservableCollection<InMemoryMovie>.DispatchCallback(context, dispatcher, new object());
215+
216+
contextCaller.Should().Be(0);
217+
functionCaller.Should().Be(1);
218+
}
172219
}

0 commit comments

Comments
 (0)