diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml index 4d9321c..36d9baf 100644 --- a/.github/workflows/dotnet-desktop.yml +++ b/.github/workflows/dotnet-desktop.yml @@ -53,6 +53,9 @@ jobs: - name: Build run: dotnet build -p:ContinuousIntegrationBuild=True --configuration ${{ env.configuration }} --no-restore + - name: Test + run: dotnet test --no-build --configuration Release --verbosity normal + - name: Publish run: dotnet publish --no-build --configuration ${{ env.configuration }} diff --git a/ColorKraken.Tests/ColorKraken.Tests.csproj b/ColorKraken.Tests/ColorKraken.Tests.csproj index 67bc80a..571829e 100644 --- a/ColorKraken.Tests/ColorKraken.Tests.csproj +++ b/ColorKraken.Tests/ColorKraken.Tests.csproj @@ -1,7 +1,7 @@ - net6.0-windows + net8.0-windows enable false @@ -11,7 +11,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/ColorKraken.Tests/EnumerableTests.cs b/ColorKraken.Tests/EnumerableTests.cs deleted file mode 100644 index 804cfff..0000000 --- a/ColorKraken.Tests/EnumerableTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Threading.Tasks; - -using Xunit; -using System.Collections.Generic; -using System; -using System.Threading; - -namespace ColorKraken.Tests -{ - public class EnumerableTests - { - [Fact] - public void DeferredExecution() - { - IEnumerable numbers = GetNumbers(3); - - //foreach(int number in numbers) - //{ - // //Do stuff - //} - - using IEnumerator numbersEnumerator = numbers.GetEnumerator(); - while(numbersEnumerator.MoveNext()) - { - int current = numbersEnumerator.Current; - //Do stuff with current - } - } - - [Fact] - public async Task AsyncDeferredExecution() - { - IAsyncEnumerable numbers = GetNumbersAsync(3); - - await foreach (int number in numbers) - { - int threadId = Thread.CurrentThread.ManagedThreadId; - //Do stuff - } - - //await using IAsyncEnumerator numbersEnumerator = numbers.GetAsyncEnumerator(); - //while (await numbersEnumerator.MoveNextAsync()) - //{ - // int current = numbersEnumerator.Current; - // //Do stuff with current - //} - } - - public IEnumerable GetNumbers(int count) - { - if (count < 0) throw new ArgumentException("TODO"); - - return GetNumbersImplementation(count); - - static IEnumerable GetNumbersImplementation(int count) - { - for (int i = 0; i < count; i++) - { - yield return i; - } - } - } - - public IAsyncEnumerable GetNumbersAsync(int count) - { - if (count < 0) throw new ArgumentException("TODO"); - - return GetNumbersImplementation(count); - - static async IAsyncEnumerable GetNumbersImplementation(int count) - { - for (int i = 0; i < count; i++) - { - await Task.Yield(); - yield return i; - } - } - } - } -} \ No newline at end of file diff --git a/ColorKraken.Tests/MainWindowViewModelTests.cs b/ColorKraken.Tests/MainWindowViewModelTests.cs index 55e1982..f9a0f60 100644 --- a/ColorKraken.Tests/MainWindowViewModelTests.cs +++ b/ColorKraken.Tests/MainWindowViewModelTests.cs @@ -100,7 +100,7 @@ public void OpenThemeFolderCommand_StartsExplorerProcess() } [Fact] - public void OnReceive_BrushUpdatedMessage_UpdatesTheme() + public async void OnReceive_BrushUpdatedMessage_UpdatesTheme() { //Arrange AutoMocker mocker = new(); @@ -112,11 +112,16 @@ public void OnReceive_BrushUpdatedMessage_UpdatesTheme() Mock themeManager = mocker.GetMock(); themeManager.Setup(x => x.SaveTheme(selectedTheme, It.IsAny>())); + themeManager.Setup(x => x.GetCategories(It.IsAny())).ReturnsAsyncEnumerable(); EditorViewModel vm = mocker.CreateInstance(); + var categoriesSet = vm.WatchPropertyChanges?>(nameof(EditorViewModel.ThemeCategories)); vm.SelectedTheme = selectedTheme; - var color = new ThemeColor("Testcolor", messenger) { Value = "new" }; - var message = new BrushUpdated(color, "old" ); + + await categoriesSet.WaitForChange(); + + var color = new ThemeColor("TestColor", messenger) { Value = "new" }; + var message = new BrushUpdated(color, "old"); //Act messenger.Send(message); diff --git a/ColorKraken.Tests/PropertyChangedHelper.cs b/ColorKraken.Tests/PropertyChangedHelper.cs new file mode 100644 index 0000000..c0b4dc1 --- /dev/null +++ b/ColorKraken.Tests/PropertyChangedHelper.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace ColorKraken.Tests; + +/// +/// Based on https://intellitect.com/blog/making-unit-testing-easier/ +/// +public static class PropertyChangedHelper +{ + public static IPropertyChanges WatchPropertyChanges(this INotifyPropertyChanged propertyChanged, string propertyName) + { + if (propertyChanged == null) throw new ArgumentNullException(nameof(propertyChanged)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + + return new PropertyChangedEnumerable(propertyChanged, propertyName); + } + + private class PropertyChangedEnumerable : IPropertyChanges + { + private readonly List _values = []; + private readonly Func _getPropertyValue; + private readonly string _propertyName; + private readonly List<(Func, TaskCompletionSource)> _waitHandles = []; + + public PropertyChangedEnumerable(INotifyPropertyChanged propertyChanged, string propertyName) + { + _propertyName = propertyName; + + const BindingFlags flags = BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.Public; + var propertyInfo = propertyChanged.GetType().GetProperty(propertyName, flags) + ?? throw new ArgumentException($"Could not find public property getter for {propertyName} on {propertyChanged.GetType().FullName}"); + var instance = Expression.Constant(propertyChanged); + var propertyExpression = Expression.Property(instance, propertyInfo); + _getPropertyValue = Expression.Lambda>(propertyExpression).Compile(); + + propertyChanged.PropertyChanged += OnPropertyChanged; + } + + private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (string.Equals(_propertyName, e.PropertyName, StringComparison.Ordinal)) + { + var value = _getPropertyValue(); + _values.Add(value); + _waitHandles.ForEach(t => + { + if (t.Item1(value)) t.Item2.SetResult(); + }); + } + } + + public IEnumerator GetEnumerator() + { + return _values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public Task WaitForChange(CancellationToken token) + { + return WaitFor(x => true, token); + } + + public Task WaitFor(Func predicate, CancellationToken token) + { + TaskCompletionSource tcs = new(); + _waitHandles.Add((predicate, tcs)); + return tcs.Task; + } + } +} + +public interface IPropertyChanges : IEnumerable +{ + Task WaitForChange(CancellationToken token = default); + Task WaitFor(Func predicate, CancellationToken token = default); +} \ No newline at end of file diff --git a/ColorKraken.Tests/TaskTests.cs b/ColorKraken.Tests/TaskTests.cs deleted file mode 100644 index 6fc0629..0000000 --- a/ColorKraken.Tests/TaskTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading.Tasks; - -using Xunit; -using System.Threading; -using System; - -namespace ColorKraken.Tests -{ - public class TaskTests - { - [Fact] - public async Task Foo() - { - int t1 = Environment.CurrentManagedThreadId; - - //await Task.Yield(); - //await Task.Delay(1); - //IOnly meaningful if the caller is the UI thread (has a synchonization context) - //await Task.Run(() => { }).ConfigureAwait(true); - await Task.CompletedTask; - - int t2 = Thread.CurrentThread.ManagedThreadId; - - Assert.Equal(t1, t2); - } - } -} \ No newline at end of file diff --git a/ColorKraken/ColorKraken.csproj b/ColorKraken/ColorKraken.csproj index cfd8c31..6534323 100644 --- a/ColorKraken/ColorKraken.csproj +++ b/ColorKraken/ColorKraken.csproj @@ -2,7 +2,7 @@ WinExe - net6.0-windows + net8.0-windows enable true Icon.ico diff --git a/ColorKraken/Configuration/TolerantSource.cs b/ColorKraken/Configuration/TolerantSource.cs index 01cae92..1b74067 100644 --- a/ColorKraken/Configuration/TolerantSource.cs +++ b/ColorKraken/Configuration/TolerantSource.cs @@ -41,7 +41,7 @@ public bool TryGet(string key, out string? value) } } - public void Set(string key, string value) + public void Set(string key, string? value) { try { @@ -73,7 +73,7 @@ public void Load() { } } - public IEnumerable GetChildKeys(IEnumerable earlierKeys, string parentPath) + public IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath) { try { @@ -82,7 +82,7 @@ public IEnumerable GetChildKeys(IEnumerable earlierKeys, string } catch (Exception) { - return Array.Empty(); + return []; } } } @@ -95,9 +95,14 @@ private class EmptyChangeToken : IChangeToken public bool ActiveChangeCallbacks => false; - public IDisposable? RegisterChangeCallback(Action callback, object state) + public IDisposable RegisterChangeCallback(Action callback, object? state) { - return null; + return new EmptyDisposable(); } } + + private sealed class EmptyDisposable : IDisposable + { + public void Dispose() { } + } } diff --git a/ColorKraken/DownloadViewModel.cs b/ColorKraken/DownloadViewModel.cs index 120c7d8..0ca3e8c 100644 --- a/ColorKraken/DownloadViewModel.cs +++ b/ColorKraken/DownloadViewModel.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Json; -using System.Security.Policy; using System.Text.Json.Serialization; using System.Threading.Tasks; using System.Windows.Data; @@ -16,10 +15,9 @@ namespace ColorKraken; -[ObservableObject] -public partial class DownloadViewModel +public partial class DownloadViewModel : ObservableObject { - public ObservableCollection Items { get; } = new(); + public ObservableCollection Items { get; } = []; private HttpClient HttpClient { get; } private IThemeManager ThemeManager { get; } diff --git a/ColorKraken/MainWindowViewModel.cs b/ColorKraken/MainWindowViewModel.cs index 265a2ba..88981f1 100644 --- a/ColorKraken/MainWindowViewModel.cs +++ b/ColorKraken/MainWindowViewModel.cs @@ -7,8 +7,7 @@ namespace ColorKraken; -[ObservableObject] -public partial class MainWindowViewModel : IRecipient +public partial class MainWindowViewModel : ObservableObject, IRecipient { public ISnackbarMessageQueue MessageQueue { get; }