Skip to content

Commit

Permalink
Merge pull request #773 from unoplatform/dev/dr/testBindable
Browse files Browse the repository at this point in the history
test(feeds): Improve ability to test Bindable<T>
  • Loading branch information
dr1rrb authored Sep 28, 2022
2 parents c288cb0 + f86f05c commit 6766ce9
Show file tree
Hide file tree
Showing 15 changed files with 305 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ public ItemsConstraint(Option<IImmutableList<T>> items)
/// <inheritdoc />
public override void Assert(IMessageEntry entry)
{
var actualItemsOpt = (Option<IImmutableList<T>>)entry.Data;
var actualItemsOpt = entry.Data;
actualItemsOpt.Type.Should().Be(_items.Type);

if (actualItemsOpt.IsSome(out var actualItems))
{
actualItems.Should().BeEquivalentTo(_items.SomeOrDefault()!);
actualItems.Should().BeAssignableTo<IImmutableList<T>>();
((IImmutableList<T>)actualItems).Should().BeEquivalentTo(_items.SomeOrDefault()!);
}
}

Expand Down
171 changes: 170 additions & 1 deletion src/Uno.Extensions.Reactive.Tests/Utils/FeedUITests.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,188 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Win32.SafeHandles;
using Uno.Extensions.Reactive.Bindings;
using Uno.Extensions.Reactive.Dispatching;
using Uno.Extensions.Reactive.Testing;

namespace Uno.Extensions.Reactive.Tests;

public class FeedUITests : FeedTests
{
private readonly Dispatcher _dispatcher = new();

/// <inheritdoc />
[TestInitialize]
public override void Initialize()
{
base.Initialize();

DispatcherHelper.GetForCurrentThread = () => default;
DispatcherHelper.GetForCurrentThread = () => _dispatcher.HasThreadAccess ? _dispatcher : null;
}

/// <inheritdoc />
[TestCleanup]
public override void Cleanup()
{
_dispatcher.Dispose();

base.Cleanup();
}

private protected async Task<T> WaitForInitialValue<TViewModel, T>(TViewModel viewModel, Func<TViewModel, Bindable<T>> propertySelector, CancellationToken ct = default)
where TViewModel : BindableViewModelBase
where T : class
{
if (!ct.CanBeCanceled)
{
ct = CT;
}

var tcs = new TaskCompletionSource<T>();
await using var _ = ct.Register(() => tcs.TrySetCanceled());
await ExecuteOnDispatcher(() =>
{
var bindable = propertySelector(viewModel);

// Note: This is a patch since the implementation of IFeed by the bindable is only a VM.SrcFeed.Select(getter).
// It should be a real implementation that synchronously reflects the current value of the bindable itself.
var currentValue = bindable.GetValue();
if (currentValue != null)
{
// This is a weak test since we could have a feed that init with null. But it's enough for now, regarding the comment above !
tcs.TrySetResult(currentValue);
return;
}

// Adding the event handler will also init the dispatcher
var ctReg = default(CancellationTokenRegistration);
viewModel.PropertyChanged += PropertyChanged;
ctReg = ct.Register(() => viewModel.PropertyChanged -= PropertyChanged);

void PropertyChanged(object? sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
if (propertyChangedEventArgs.PropertyName == bindable.PropertyName)
{
ctReg.Dispose();
viewModel.PropertyChanged -= PropertyChanged;
tcs.TrySetResult(bindable.GetValue());
}
}
},
ct);

return await tcs.Task;
}

private protected async ValueTask ExecuteOnDispatcher(Action action, CancellationToken ct = default)
{
if (!ct.CanBeCanceled)
{
ct = CT;
}

var tcs = new TaskCompletionSource();
await using var _ = ct.Register(() => tcs.TrySetCanceled());
_dispatcher.TryEnqueue(() =>
{
action();
tcs.TrySetResult();
});

await tcs.Task;
}

private protected async ValueTask ExecuteAsyncOnDispatcher(AsyncAction action, CancellationToken ct = default)
{
if (!ct.CanBeCanceled)
{
ct = CT;
}

var tcs = new TaskCompletionSource();
await using var _ = ct.Register(() => tcs.TrySetCanceled());
_dispatcher.TryEnqueue(async () =>
{
await action(ct);
tcs.TrySetResult();
});

await tcs.Task;
}

private class Dispatcher : IDispatcherInternal, IDisposable
{
private readonly Thread _thread;
private readonly Queue<Action> _queue = new();
private readonly AutoResetEvent _evt = new(false);

private bool _isDisposed;

public Dispatcher()
{
_thread = new Thread(Run);
_thread.Start();
}

/// <inheritdoc />
public bool HasThreadAccess => Thread.CurrentThread == _thread;

/// <inheritdoc />
public void TryEnqueue(Action action)
{
if (_isDisposed)
{
throw new InvalidOperationException("Dispatcher has already been aborted!");
}

lock (_queue)
{
_queue.Enqueue(action);
}

_evt.Set();
}

private void Run()
{
while (!_isDisposed)
{
try
{
bool hasItem;
Action? item;
lock (_queue)
{
hasItem = _queue.TryDequeue(out item);
}

if (hasItem)
{
item!();
}
else
{
_evt.WaitOne();
}
}
catch (Exception error)
{
throw new InvalidOperationException("Got an exception on the UI thread", error);
}
}
}

public void Dispose()
{
_isDisposed = true;
_evt.Set();
_thread.Join();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class Bindable<T> : IBindable, INotifyPropertyChanged, IFeed<T>
private readonly bool _hasValueProperty;
private readonly bool _isInherited;

internal string PropertyName => _property.Name;

internal bool CanWrite => _property.CanWrite;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ internal BindablePropertyInfo(
_update = setter;
}

internal string Name => _name;

internal bool IsValid => _owner is not null;

internal bool CanWrite => _update is not null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
using Uno.Extensions.Collections;
using Uno.Extensions.Collections.Tracking;
using Uno.Extensions.Reactive.Bindings.Collections._BindableCollection.Facets;

#if WINUI
using ISchedulerInfo = Microsoft.UI.Dispatching.DispatcherQueue;
#else
using ISchedulerInfo = Windows.System.DispatcherQueue;
#endif
using Uno.Extensions.Reactive.Dispatching;

namespace Uno.Extensions.Reactive.Bindings.Collections._BindableCollection.Data
{
Expand All @@ -20,7 +15,7 @@ internal sealed class DataLayer : ILayerHolder, IBindableCollectionViewSource, I
{
private readonly DataLayer? _parent;
private readonly IServiceProvider? _services;
private readonly ISchedulerInfo? _context;
private readonly IDispatcherInternal? _context;
private readonly IBindableCollectionDataLayerStrategy _layerStrategy;
private readonly IEnumerable<object> _facets;

Expand All @@ -46,7 +41,7 @@ public static DataLayer Create(
IBindableCollectionDataLayerStrategy layerStrategy,
IObservableCollection items,
IServiceProvider? services,
ISchedulerInfo? context)
IDispatcherInternal? context)
{
var holder = new DataLayer(null, services, layerStrategy, context);
var initContext = layerStrategy.CreateUpdateContext(VisitorType.InitializeCollection, TrackingMode.Reset);
Expand All @@ -70,7 +65,7 @@ public static DataLayer Create(
return (holder, initializer);
}

private DataLayer(DataLayer? parent, IServiceProvider? services, IBindableCollectionDataLayerStrategy layerStrategy, ISchedulerInfo? context)
private DataLayer(DataLayer? parent, IServiceProvider? services, IBindableCollectionDataLayerStrategy layerStrategy, IDispatcherInternal? context)
{
_context = context;
_parent = parent;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Uno.Extensions.Reactive.Dispatching;

/// <summary>
/// An helper class to create a <see cref="DispatcherHelper.FindDispatcher"/> with the ability
/// to asynchronously get notified when the first dispatcher is being resolved.
/// </summary>
internal sealed class AsyncLazyDispatcherProvider : IDisposable
{
private readonly DispatcherHelper.FindDispatcher _dispatcherProvider;
private readonly TaskCompletionSource<IDispatcherInternal> _first = new();

public AsyncLazyDispatcherProvider(DispatcherHelper.FindDispatcher? dispatcherProvider = null)
{
_dispatcherProvider = dispatcherProvider ?? DispatcherHelper.GetForCurrentThread;
}

public bool TryResolve()
=> FindDispatcher() is not null;

public Task<IDispatcherInternal> GetFirstResolved(CancellationToken ct)
=> _first.Task;

public IDispatcherInternal? FindDispatcher()
{
if (_dispatcherProvider() is { } dispatcher)
{
_first.TrySetResult(dispatcher);

return dispatcher;
}
else
{
return null;
}
}

/// <inheritdoc />
public void Dispose()
=> _first.TrySetCanceled();
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
using System;
using System.Linq;
using Windows.ApplicationModel.Core;

namespace Uno.Extensions.Reactive.Dispatching;

internal class DispatcherHelper
{
public delegate DispatcherQueue? FindDispatcher();
public delegate IDispatcherInternal? FindDispatcher();

public static DispatcherQueue GetDispatcher()
public static IDispatcherInternal GetDispatcher()
=> GetDispatcher(null);

public static DispatcherQueue GetDispatcher(DispatcherQueue? given)
public static IDispatcherInternal GetDispatcher(IDispatcherInternal? given)
=> given
?? GetForCurrentThread()
#if !WINUI
?? CoreApplication.MainView?.DispatcherQueue
#endif
?? throw new InvalidOperationException("Failed to get dispatcher to use. Either explicitly provide the dispatcher to use, either make sure to invoke this on the UI thread.");

public static FindDispatcher GetForCurrentThread = DispatcherQueue.GetForCurrentThread;
public static FindDispatcher GetForCurrentThread = DispatcherQueueProvider.GetForCurrentThread;
}
Loading

0 comments on commit 6766ce9

Please sign in to comment.