From 5aa5c7094f2e189896a6bec87324411602342075 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 4 Apr 2025 15:57:54 +0200 Subject: [PATCH 1/5] feat: enhance OperationResult and OperationError with implicit operators and safe execution method --- .../OperationResult/OperationResultTests.cs | 125 +++++++++++++++++- .../Shared/OperationResult/OperationError.cs | 12 +- .../Shared/OperationResult/OperationResult.cs | 43 +++++- .../OperationResult/OperationResult`1.cs | 13 +- 4 files changed, 179 insertions(+), 14 deletions(-) diff --git a/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs b/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs index 03117e95..ac922668 100644 --- a/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs +++ b/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs @@ -1,6 +1,6 @@ using WheelWizard.Shared; -namespace WheelWizard.Test; +namespace WheelWizard.Test.Shared.OperationResultTests; public class OperationResultTests { @@ -88,7 +88,7 @@ public void NewSuccessGenericResult_ShouldHaveCorrectState() Assert.True(operationResult.IsSuccess); Assert.Equal(value, operationResult.Value); } - + [Fact(DisplayName = "Create success generic result, should have correct state")] public void CreateSuccessGenericResult_ShouldHaveCorrectState() { @@ -104,7 +104,7 @@ public void CreateSuccessGenericResult_ShouldHaveCorrectState() Assert.True(operationResult.IsSuccess); Assert.Equal(value, operationResult.Value); } - + [Fact(DisplayName = "New failure generic result, should have correct state")] public void NewFailureGenericResult_ShouldHaveCorrectState() { @@ -119,7 +119,7 @@ public void NewFailureGenericResult_ShouldHaveCorrectState() Assert.True(operationResult.IsFailure); Assert.False(operationResult.IsSuccess); } - + [Fact(DisplayName = "Create failure generic result, should have correct state")] public void CreateFailureGenericResult_ShouldHaveCorrectState() { @@ -134,7 +134,7 @@ public void CreateFailureGenericResult_ShouldHaveCorrectState() Assert.True(operationResult.IsFailure); Assert.False(operationResult.IsSuccess); } - + [Fact(DisplayName = "Implicit generic result from error, should have correct state")] public void ImplicitGenericResultFromError_ShouldHaveCorrectState() { @@ -149,7 +149,7 @@ public void ImplicitGenericResultFromError_ShouldHaveCorrectState() Assert.True(operationResult.IsFailure); Assert.False(operationResult.IsSuccess); } - + [Fact(DisplayName = "Implicit generic result from value, should have correct state")] public void ImplicitGenericResultFromValue_ShouldHaveCorrectState() { @@ -165,4 +165,117 @@ public void ImplicitGenericResultFromValue_ShouldHaveCorrectState() Assert.True(operationResult.IsSuccess); Assert.Equal(value, operationResult.Value); } + + [Fact(DisplayName = "Implicit result from string, should have failed state")] + public void ImplicitResultFromString_ShouldHaveFailedState() + { + // Arrange + const string errorMessage = "Error message"; + + // Act + OperationResult operationResult = errorMessage; + + // Assert + Assert.NotNull(operationResult.Error); + Assert.True(operationResult.IsFailure); + Assert.False(operationResult.IsSuccess); + Assert.Equal(errorMessage, operationResult.Error?.Message); + } + + [Fact(DisplayName = "Implicit result from exception, should have failed state")] + public void ImplicitResultFromException_ShouldHaveFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + // Act + OperationResult operationResult = exception; + + // Assert + Assert.NotNull(operationResult.Error); + Assert.True(operationResult.IsFailure); + Assert.False(operationResult.IsSuccess); + Assert.Equal(exception.Message, operationResult.Error?.Message); + } + + [Fact(DisplayName = "Implicit generic result from string, should have correct failed state")] + public void ImplicitGenericResultFromString_ShouldHaveCorrectFailedState() + { + // Arrange + const string errorMessage = "Error message"; + + // Act + OperationResult operationResult = errorMessage; + + // Assert + Assert.NotNull(operationResult.Error); + Assert.True(operationResult.IsFailure); + Assert.False(operationResult.IsSuccess); + Assert.Equal(errorMessage, operationResult.Error?.Message); + } + + [Fact(DisplayName = "Implicit generic result from exception, should have correct failed state")] + public void ImplicitGenericResultFromException_ShouldHaveCorrectFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + // Act + OperationResult operationResult = exception; + + // Assert + Assert.NotNull(operationResult.Error); + Assert.True(operationResult.IsFailure); + Assert.False(operationResult.IsSuccess); + Assert.Equal(exception.Message, operationResult.Error?.Message); + } + + [Fact(DisplayName = "Safe execute without exception, should have correct success state")] + public void SafeExecuteWithoutException_ShouldHaveCorrectSuccessState() + { + // Arrange + int Func() => 42; + + // Act + var result = OperationResult.SafeExecute(Func); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(42, result.Value); + } + + [Fact(DisplayName = "Safe execute with exception, should have failed state")] + public void SafeExecuteWithException_ShouldHaveFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + int Func() => throw exception; + + // Act + var result = OperationResult.SafeExecute(Func); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(exception.Message, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Safe execute with exception with override, should have failed state with message")] + public void SafeExecuteWithExceptionWithOverride_ShouldHaveFailedStateWithMessage() + { + // Arrange + var exception = new Exception("Error message"); + const string errorMessage = "Custom error message"; + + int Func() => throw exception; + + // Act + var result = OperationResult.SafeExecute(Func, errorMessage); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(errorMessage, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } } diff --git a/WheelWizard/Shared/OperationResult/OperationError.cs b/WheelWizard/Shared/OperationResult/OperationError.cs index 3c8647d1..c05a1c25 100644 --- a/WheelWizard/Shared/OperationResult/OperationError.cs +++ b/WheelWizard/Shared/OperationResult/OperationError.cs @@ -15,5 +15,15 @@ public class OperationError /// public Exception? Exception { get; init; } - public static implicit operator OperationError(string message) => new() { Message = message }; + #region Implicit Operators + + public static implicit operator OperationError(string errorMessage) => new() { Message = errorMessage }; + + public static implicit operator OperationError(Exception exception) => new() + { + Message = exception.Message, + Exception = exception + }; + + #endregion } diff --git a/WheelWizard/Shared/OperationResult/OperationResult.cs b/WheelWizard/Shared/OperationResult/OperationResult.cs index c8944b50..fcdd0abb 100644 --- a/WheelWizard/Shared/OperationResult/OperationResult.cs +++ b/WheelWizard/Shared/OperationResult/OperationResult.cs @@ -43,6 +43,8 @@ public OperationResult(OperationError error) Error = error; } + #region Creation Methods + /// /// Creates a new instance of the class with the specified error. /// @@ -62,7 +64,7 @@ public OperationResult(OperationError error) /// The error that occurred during the operation. /// The type of the value. /// A new instance of the class. - public static OperationResult Fail(OperationError error) => new(error); + public static OperationResult Fail(OperationError error) where T : notnull => new(error); /// /// Creates a new instance of the class that indicates success. @@ -70,8 +72,41 @@ public OperationResult(OperationError error) /// The value of the operation result. /// The type of the value. /// A new instance of the class. - public static OperationResult Ok(T value) => new(value); + public static OperationResult Ok(T value) where T : notnull => new(value); + + /// + /// Executes the specified function and returns the result. + /// Catches any exceptions thrown by the function and returns a failure result. + /// + /// The function to execute. + /// The error message to return if the function fails. + /// The type of the value. + /// An that indicates the result of the operation. + public static OperationResult SafeExecute(Func func, string? errorMessage = null) where T : notnull + { + try + { + return Ok(func()); + } + catch (Exception ex) + { + return Fail(new() + { + Message = errorMessage ?? ex.Message, + Exception = ex + }); + } + } + + #endregion + + #region Implicit Operators + + public static implicit operator OperationResult(OperationError error) => Fail(error); + + public static implicit operator OperationResult(string errorMessage) => Fail(errorMessage); + + public static implicit operator OperationResult(Exception exception) => Fail(exception); - public static implicit operator OperationResult(OperationError error) => new(error); - public static implicit operator OperationResult(string errorMessage) => new(errorMessage); + #endregion } diff --git a/WheelWizard/Shared/OperationResult/OperationResult`1.cs b/WheelWizard/Shared/OperationResult/OperationResult`1.cs index 2ea73757..958ccf31 100644 --- a/WheelWizard/Shared/OperationResult/OperationResult`1.cs +++ b/WheelWizard/Shared/OperationResult/OperationResult`1.cs @@ -36,8 +36,15 @@ public OperationResult(OperationError error) : base(error) { } - public static implicit operator OperationResult(OperationError error) => new(error); - public static implicit operator OperationResult(string errorMessage) => new(errorMessage); + #region Implicit Operators - public static implicit operator OperationResult(T value) => new(value); + public static implicit operator OperationResult(T value) => Ok(value); + + public static implicit operator OperationResult(OperationError error) => Fail(error); + + public static implicit operator OperationResult(string errorMessage) => Fail(errorMessage); + + public static implicit operator OperationResult(Exception exception) => Fail(exception); + + #endregion } From d53ca618ef713986df9d674b99e15b1d7723a352 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 4 Apr 2025 16:36:05 +0200 Subject: [PATCH 2/5] feat: make operation result nullable --- WheelWizard/Shared/OperationResult/OperationResult`1.cs | 2 +- WheelWizard/Shared/Services/ApiCaller.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WheelWizard/Shared/OperationResult/OperationResult`1.cs b/WheelWizard/Shared/OperationResult/OperationResult`1.cs index 958ccf31..99910326 100644 --- a/WheelWizard/Shared/OperationResult/OperationResult`1.cs +++ b/WheelWizard/Shared/OperationResult/OperationResult`1.cs @@ -6,7 +6,7 @@ namespace WheelWizard.Shared; /// Represents the result of an operation. /// /// The type of the value. -public class OperationResult : OperationResult +public class OperationResult : OperationResult where T : notnull { [MemberNotNullWhen(true, nameof(Value))] public override bool IsSuccess => base.IsSuccess; diff --git a/WheelWizard/Shared/Services/ApiCaller.cs b/WheelWizard/Shared/Services/ApiCaller.cs index feed780f..49d8f422 100644 --- a/WheelWizard/Shared/Services/ApiCaller.cs +++ b/WheelWizard/Shared/Services/ApiCaller.cs @@ -15,12 +15,12 @@ public interface IApiCaller where TApi : class /// The API method to call. /// The type of the result. /// An representing the result of the API call. - Task> CallApiAsync(Expression>> apiCall); + Task> CallApiAsync(Expression>> apiCall) where TResult : notnull; } public class ApiCaller(IServiceScopeFactory scopeFactory, ILogger> logger) : IApiCaller where T : class { - public async Task> CallApiAsync(Expression>> apiCall) + public async Task> CallApiAsync(Expression>> apiCall) where TResult : notnull { var apiCallString = apiCall.Body.ToString(); @@ -29,7 +29,7 @@ public async Task> CallApiAsync(Expression(); - return await apiCall.Compile().Invoke(api); + return Ok(await apiCall.Compile().Invoke(api)); } catch (Exception exception) { From 6a76b4c65ef6bf9343802360bff73b9e6ab72056 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 4 Apr 2025 18:17:33 +0200 Subject: [PATCH 3/5] feat: add async and synchronous SafeExecute methods for improved error handling --- .../Shared/OperationResult/OperationResult.cs | 82 ++++++++++++++++++- .../OperationResult/OperationResult`1.cs | 18 ++-- 2 files changed, 85 insertions(+), 15 deletions(-) diff --git a/WheelWizard/Shared/OperationResult/OperationResult.cs b/WheelWizard/Shared/OperationResult/OperationResult.cs index fcdd0abb..2e5b9442 100644 --- a/WheelWizard/Shared/OperationResult/OperationResult.cs +++ b/WheelWizard/Shared/OperationResult/OperationResult.cs @@ -64,7 +64,7 @@ public OperationResult(OperationError error) /// The error that occurred during the operation. /// The type of the value. /// A new instance of the class. - public static OperationResult Fail(OperationError error) where T : notnull => new(error); + public static OperationResult Fail(OperationError error) => new(error); /// /// Creates a new instance of the class that indicates success. @@ -72,7 +72,7 @@ public OperationResult(OperationError error) /// The value of the operation result. /// The type of the value. /// A new instance of the class. - public static OperationResult Ok(T value) where T : notnull => new(value); + public static OperationResult Ok(T value) => new(value); /// /// Executes the specified function and returns the result. @@ -82,11 +82,12 @@ public OperationResult(OperationError error) /// The error message to return if the function fails. /// The type of the value. /// An that indicates the result of the operation. - public static OperationResult SafeExecute(Func func, string? errorMessage = null) where T : notnull + public static OperationResult SafeExecute(Func func, string? errorMessage = null) { try { - return Ok(func()); + var value = func(); + return Ok(value); } catch (Exception ex) { @@ -98,6 +99,79 @@ public static OperationResult SafeExecute(Func func, string? errorMessa } } + /// + /// Executes the specified function and returns the result. + /// Catches any exceptions thrown by the function and returns a failure result. + /// + /// The function to execute. + /// The error message to return if the function fails. + /// The type of the value. + /// An that indicates the result of the operation. + public static async Task> SafeExecute(Func> func, string? errorMessage = null) + { + try + { + var value = await func(); + return Ok(value); + } + catch (Exception ex) + { + return Fail(new() + { + Message = errorMessage ?? ex.Message, + Exception = ex + }); + } + } + + /// + /// Executes the specified function and returns the result. + /// Catches any exceptions thrown by the function and returns a failure result. + /// + /// The action to execute. + /// The error message to return if the function fails. + /// An that indicates the result of the operation. + public static OperationResult SafeExecute(Action action, string? errorMessage = null) + { + try + { + action(); + return Ok(); + } + catch (Exception ex) + { + return Fail(new() + { + Message = errorMessage ?? ex.Message, + Exception = ex + }); + } + } + + /// + /// Executes the specified function and returns the result. + /// Catches any exceptions thrown by the function and returns a failure result. + /// + /// The action to execute. + /// The error message to return if the function fails. + /// An that indicates the result of the operation. + public static async Task SafeExecute(Func action, string? errorMessage = null) + { + try + { + await action(); + return Ok(); + } + catch (Exception ex) + { + return Fail(new() + { + Message = errorMessage ?? ex.Message, + Exception = ex + }); + } + } + #endregion #region Implicit Operators diff --git a/WheelWizard/Shared/OperationResult/OperationResult`1.cs b/WheelWizard/Shared/OperationResult/OperationResult`1.cs index 99910326..335e3845 100644 --- a/WheelWizard/Shared/OperationResult/OperationResult`1.cs +++ b/WheelWizard/Shared/OperationResult/OperationResult`1.cs @@ -1,23 +1,18 @@ -using System.Diagnostics.CodeAnalysis; - -namespace WheelWizard.Shared; +namespace WheelWizard.Shared; /// /// Represents the result of an operation. /// /// The type of the value. -public class OperationResult : OperationResult where T : notnull +public class OperationResult : OperationResult { - [MemberNotNullWhen(true, nameof(Value))] - public override bool IsSuccess => base.IsSuccess; - - [MemberNotNullWhen(false, nameof(Value))] - public override bool IsFailure => base.IsFailure; + private readonly T _value; /// /// The value of the operation result. /// - public T? Value { get; } + /// Thrown if the operation was not successful. + public T Value => IsSuccess ? _value : throw new InvalidOperationException("The operation was not successful."); /// /// Initializes a new instance of the class. @@ -25,7 +20,7 @@ public class OperationResult : OperationResult where T : notnull /// The value of the operation result. public OperationResult(T value) { - Value = value; + _value = value; } /// @@ -34,6 +29,7 @@ public OperationResult(T value) /// The error that occurred during the operation. public OperationResult(OperationError error) : base(error) { + _value = default!; } #region Implicit Operators From bce6e2fc57f7c49b9348ba89c15916297d1b105a Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 4 Apr 2025 18:24:46 +0200 Subject: [PATCH 4/5] feat: refactor API call methods to use SafeExecute for improved error handling --- .../Platforms/LinuxUpdatePlatform.cs | 61 +++++++------------ .../Platforms/WindowsUpdatePlatform.cs | 25 ++------ WheelWizard/Shared/Services/ApiCaller.cs | 33 +++++----- 3 files changed, 40 insertions(+), 79 deletions(-) diff --git a/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs b/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs index 19a519ac..568ff358 100644 --- a/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs +++ b/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs @@ -30,13 +30,13 @@ public async Task ExecuteUpdateAsync(string downloadUrl) { var currentExecutablePath = Environment.ProcessPath; if (currentExecutablePath is null) - return Fail(Phrases.PopupText_UnableUpdateWhWz_ReasonLocation); + return Phrases.PopupText_UnableUpdateWhWz_ReasonLocation; var currentExecutableName = fileSystem.Path.GetFileName(currentExecutablePath); var currentFolder = fileSystem.Path.GetDirectoryName(currentExecutablePath); if (currentFolder is null) - return Fail(Phrases.PopupText_UnableUpdateWhWz_ReasonLocation); + return Phrases.PopupText_UnableUpdateWhWz_ReasonLocation; // Download the new executable to a temporary file. var newFilePath = fileSystem.Path.Combine(currentFolder, currentExecutableName + "_new"); @@ -67,7 +67,7 @@ private OperationResult CreateAndRunShellScript(string currentFilePath, string n { var currentFolder = fileSystem.Path.GetDirectoryName(currentFilePath); if (currentFolder is null) - return Fail(Phrases.PopupText_UnableUpdateWhWz_ReasonLocation); + return Phrases.PopupText_UnableUpdateWhWz_ReasonLocation; var scriptFilePath = fileSystem.Path.Combine(currentFolder, "update.sh"); var originalFileName = fileSystem.Path.GetFileName(currentFilePath); @@ -98,30 +98,24 @@ sleep 1 fileSystem.File.WriteAllText(scriptFilePath, scriptContent); // Ensure the script is executable. - try - { - Process.Start(new ProcessStartInfo - { - FileName = "/usr/bin/env", - ArgumentList = + var chmodResult = SafeExecute(() => + Process.Start(new ProcessStartInfo { - "chmod", - "+x", - "--", - scriptFilePath, - }, - CreateNoWindow = true, - UseShellExecute = false - })?.WaitForExit(); - } - catch (Exception ex) - { - return Fail(new() - { - Message = "Failed to set execute permission for the update script.", - Exception = ex - }); - } + FileName = "/usr/bin/env", + ArgumentList = + { + "chmod", + "+x", + "--", + scriptFilePath, + }, + CreateNoWindow = true, + UseShellExecute = false + })?.WaitForExit(), errorMessage: "Failed to set execute permission for the update script." + ); + + if (chmodResult.IsFailure) + return chmodResult; var processStartInfo = new ProcessStartInfo { @@ -137,19 +131,6 @@ sleep 1 WorkingDirectory = currentFolder }; - try - { - Process.Start(processStartInfo); - } - catch (Exception ex) - { - return Fail(new() - { - Message = "Failed to execute the update script.", - Exception = ex - }); - } - - return Ok(); + return SafeExecute(() => Process.Start(processStartInfo), errorMessage: "Failed to execute the update script."); } } diff --git a/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs b/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs index 6bfbea12..aa25319f 100644 --- a/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs +++ b/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs @@ -44,16 +44,11 @@ private static OperationResult RestartAsAdmin() Verb = "runas" // This verb asks for elevation. }; - try + return SafeExecute(() => { Process.Start(startInfo); Environment.Exit(0); - return Ok(); - } - catch (Exception) - { - return Phrases.PopupText_RestartAdminFail; - } + }, errorMessage: Phrases.PopupText_RestartAdminFail); } private static bool IsAdministrator() @@ -108,7 +103,7 @@ private OperationResult CreateAndRunPowerShellScript(string currentFilePath, str { var currentFolder = fileSystem.Path.GetDirectoryName(currentFilePath); if (currentFolder is null) - return Fail(Phrases.PopupText_UnableUpdateWhWz_ReasonLocation); + return Phrases.PopupText_UnableUpdateWhWz_ReasonLocation; var scriptFilePath = fileSystem.Path.Combine(currentFolder, "update.ps1"); var originalFileName = fileSystem.Path.GetFileName(currentFilePath); @@ -179,18 +174,6 @@ exit 1 WorkingDirectory = currentFolder }; - try - { - Process.Start(processStartInfo); - return Ok(); - } - catch (Exception ex) - { - return Fail(new() - { - Message = "Failed to execute the update script.", - Exception = ex - }); - } + return SafeExecute(() => Process.Start(processStartInfo), errorMessage: "Failed to execute the update script."); } } diff --git a/WheelWizard/Shared/Services/ApiCaller.cs b/WheelWizard/Shared/Services/ApiCaller.cs index 49d8f422..eeebb3e5 100644 --- a/WheelWizard/Shared/Services/ApiCaller.cs +++ b/WheelWizard/Shared/Services/ApiCaller.cs @@ -12,33 +12,30 @@ public interface IApiCaller where TApi : class /// /// Calls the specified API method asynchronously. /// - /// The API method to call. + /// The API method to call. Make sure you name the variable clearly as it is used in the logs. /// The type of the result. /// An representing the result of the API call. - Task> CallApiAsync(Expression>> apiCall) where TResult : notnull; + /// + /// Uses the expression and the name of for logging purposes. + /// + Task> CallApiAsync(Expression>> apiCall); } public class ApiCaller(IServiceScopeFactory scopeFactory, ILogger> logger) : IApiCaller where T : class { - public async Task> CallApiAsync(Expression>> apiCall) where TResult : notnull + public async Task> CallApiAsync(Expression>> apiCall) { var apiCallString = apiCall.Body.ToString(); + var apiCallFunction = apiCall.Compile(); + var apiName = typeof(T).Name[1..]; - try - { - using var scope = scopeFactory.CreateScope(); - var api = scope.ServiceProvider.GetRequiredService(); + using var scope = scopeFactory.CreateScope(); + var api = scope.ServiceProvider.GetRequiredService(); - return Ok(await apiCall.Compile().Invoke(api)); - } - catch (Exception exception) - { - logger.LogError(exception, "API call '{ApiCall}' failed: {Message}", apiCallString, exception.Message); - return new OperationError - { - Message = $"API call '{apiCallString}' failed: {exception.Message}", - Exception = exception - }; - } + var result = await SafeExecute(async () => await apiCallFunction.Invoke(api), errorMessage: $"{apiName} call failed"); + if (!result.IsSuccess) + logger.LogError(result.Error.Exception, "API method '{ApiCall}' failed: {Message}", apiCallString, result.Error.Message); + + return result; } } From aa9ec8b468abf7f7377491169c931bc89c532143 Mon Sep 17 00:00:00 2001 From: Ruben Date: Fri, 4 Apr 2025 18:56:41 +0200 Subject: [PATCH 5/5] feat: rename SafeExecute to TryCatch for consistency and clarity in error handling --- WheelWizard.Test/GlobalUsings.cs | 1 + .../OperationResult/OperationResultTests.cs | 182 ++++++++++++++++-- .../Services/AvaloniaLoggerAdapterTests.cs | 94 +++++++++ .../Platforms/LinuxUpdatePlatform.cs | 4 +- .../Platforms/WindowsUpdatePlatform.cs | 4 +- .../Shared/OperationResult/OperationResult.cs | 8 +- WheelWizard/Shared/Services/ApiCaller.cs | 2 +- 7 files changed, 271 insertions(+), 24 deletions(-) create mode 100644 WheelWizard.Test/Shared/Services/AvaloniaLoggerAdapterTests.cs diff --git a/WheelWizard.Test/GlobalUsings.cs b/WheelWizard.Test/GlobalUsings.cs index 6ac12af2..c728b1f1 100644 --- a/WheelWizard.Test/GlobalUsings.cs +++ b/WheelWizard.Test/GlobalUsings.cs @@ -1 +1,2 @@ global using NSubstitute; +global using static WheelWizard.Shared.OperationResult; diff --git a/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs b/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs index ac922668..cf1035cb 100644 --- a/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs +++ b/WheelWizard.Test/Shared/OperationResult/OperationResultTests.cs @@ -2,13 +2,15 @@ namespace WheelWizard.Test.Shared.OperationResultTests; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + public class OperationResultTests { [Fact(DisplayName = "Create success result, should have correct state")] public void CreateSuccessResult_ShouldHaveCorrectState() { // Act - var operationResult = OperationResult.Ok(); + var operationResult = Ok(); // Assert Assert.Null(operationResult.Error); @@ -50,7 +52,7 @@ public void CreateFailureResult_ShouldHaveCorrectState() var error = new OperationError { Message = "Error message" }; // Act - var operationResult = OperationResult.Fail(error); + var operationResult = Fail(error); // Assert Assert.Equal(error, operationResult.Error); @@ -96,7 +98,7 @@ public void CreateSuccessGenericResult_ShouldHaveCorrectState() var value = new object(); // Act - var operationResult = OperationResult.Ok(value); + var operationResult = Ok(value); // Assert Assert.Null(operationResult.Error); @@ -127,7 +129,7 @@ public void CreateFailureGenericResult_ShouldHaveCorrectState() var error = new OperationError { Message = "Error message" }; // Act - var operationResult = OperationResult.Fail(error); + var operationResult = Fail(error); // Assert Assert.Equal(error, operationResult.Error); @@ -165,7 +167,7 @@ public void ImplicitGenericResultFromValue_ShouldHaveCorrectState() Assert.True(operationResult.IsSuccess); Assert.Equal(value, operationResult.Value); } - + [Fact(DisplayName = "Implicit result from string, should have failed state")] public void ImplicitResultFromString_ShouldHaveFailedState() { @@ -181,7 +183,7 @@ public void ImplicitResultFromString_ShouldHaveFailedState() Assert.False(operationResult.IsSuccess); Assert.Equal(errorMessage, operationResult.Error?.Message); } - + [Fact(DisplayName = "Implicit result from exception, should have failed state")] public void ImplicitResultFromException_ShouldHaveFailedState() { @@ -230,22 +232,72 @@ public void ImplicitGenericResultFromException_ShouldHaveCorrectFailedState() Assert.Equal(exception.Message, operationResult.Error?.Message); } - [Fact(DisplayName = "Safe execute without exception, should have correct success state")] - public void SafeExecuteWithoutException_ShouldHaveCorrectSuccessState() + [Fact(DisplayName = "Try catch without exception, should have correct success state")] + public void TryCatchWithoutException_ShouldHaveCorrectSuccessState() + { + // Arrange + void Action() + { + } + + // Act + var result = TryCatch(Action); + + // Assert + Assert.True(result.IsSuccess); + } + + [Fact(DisplayName = "Try catch with exception, should have failed state")] + public void TryCatchWithException_ShouldHaveFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + void Action() => throw exception; + + // Act + var result = TryCatch(Action); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(exception.Message, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Try catch with exception with override, should have failed state with message")] + public void TryCatchWithExceptionWithOverride_ShouldHaveFailedStateWithMessage() + { + // Arrange + var exception = new Exception("Error message"); + const string errorMessage = "Custom error message"; + + // Act + void Action() => throw exception; + + var result = TryCatch(Action, errorMessage); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(errorMessage, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Generic try catch without exception, should have correct success state")] + public void GenericTryCatchWithoutException_ShouldHaveCorrectSuccessState() { // Arrange int Func() => 42; // Act - var result = OperationResult.SafeExecute(Func); + var result = TryCatch(Func); // Assert Assert.True(result.IsSuccess); Assert.Equal(42, result.Value); } - [Fact(DisplayName = "Safe execute with exception, should have failed state")] - public void SafeExecuteWithException_ShouldHaveFailedState() + [Fact(DisplayName = "Generic try catch with exception, should have failed state")] + public void GenericTryCatchWithException_ShouldHaveFailedState() { // Arrange var exception = new Exception("Error message"); @@ -253,7 +305,7 @@ public void SafeExecuteWithException_ShouldHaveFailedState() int Func() => throw exception; // Act - var result = OperationResult.SafeExecute(Func); + var result = TryCatch(Func); // Assert Assert.True(result.IsFailure); @@ -261,8 +313,8 @@ public void SafeExecuteWithException_ShouldHaveFailedState() Assert.Equal(exception, result.Error?.Exception); } - [Fact(DisplayName = "Safe execute with exception with override, should have failed state with message")] - public void SafeExecuteWithExceptionWithOverride_ShouldHaveFailedStateWithMessage() + [Fact(DisplayName = "Generic try catch with exception with override, should have failed state with message")] + public void GenericTryCatchWithExceptionWithOverride_ShouldHaveFailedStateWithMessage() { // Arrange var exception = new Exception("Error message"); @@ -271,7 +323,105 @@ public void SafeExecuteWithExceptionWithOverride_ShouldHaveFailedStateWithMessag int Func() => throw exception; // Act - var result = OperationResult.SafeExecute(Func, errorMessage); + var result = TryCatch(Func, errorMessage); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(errorMessage, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Generic safe execute async without exception, should have success state")] + public async Task GenericTryCatchAsyncWithoutException_ShouldHaveSuccessState() + { + // Arrange + const int expectedValue = 42; + + async Task Func() => await Task.FromResult(expectedValue); + + // Act + var result = await TryCatch(Func); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(expectedValue, result.Value); + } + + + [Fact(DisplayName = "Generic safe execute async with exception, should have failed state")] + public async Task GenericTryCatchAsyncWithException_ShouldHaveFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + async Task Func() => throw exception; + + // Act + var result = await TryCatch(Func); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(exception.Message, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Generic safe execute async with exception with override, should have failed state with message")] + public async Task GenericTryCatchAsyncWithExceptionWithOverride_ShouldHaveFailedStateWithMessage() + { + // Arrange + var exception = new Exception("Error message"); + const string errorMessage = "Custom error message"; + async Task Func() => throw exception; + + // Act + var result = await TryCatch(Func, errorMessage); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(errorMessage, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Try catch async without exception, should have success state")] + public async Task TryCatchAsyncWithoutException_ShouldHaveSuccessState() + { + // Arrange + async Task Func() => await Task.CompletedTask; + + // Act + var result = await TryCatch(Func); + + // Assert + Assert.True(result.IsSuccess); + } + + [Fact(DisplayName = "Try catch async with exception, should have failed state")] + public async Task TryCatchAsyncWithException_ShouldHaveFailedState() + { + // Arrange + var exception = new Exception("Error message"); + + async Task Func() => throw exception; + + // Act + var result = await TryCatch(Func); + + // Assert + Assert.True(result.IsFailure); + Assert.Equal(exception.Message, result.Error?.Message); + Assert.Equal(exception, result.Error?.Exception); + } + + [Fact(DisplayName = "Try catch async with exception with override, should have failed state with message")] + public async Task TryCatchAsyncWithExceptionWithOverride_ShouldHaveFailedStateWithMessage() + { + // Arrange + var exception = new Exception("Error message"); + const string errorMessage = "Custom error message"; + + async Task Func() => throw exception; + // Act + var result = await TryCatch(Func, errorMessage); // Assert Assert.True(result.IsFailure); @@ -279,3 +429,5 @@ public void SafeExecuteWithExceptionWithOverride_ShouldHaveFailedStateWithMessag Assert.Equal(exception, result.Error?.Exception); } } + +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously diff --git a/WheelWizard.Test/Shared/Services/AvaloniaLoggerAdapterTests.cs b/WheelWizard.Test/Shared/Services/AvaloniaLoggerAdapterTests.cs new file mode 100644 index 00000000..00bb199b --- /dev/null +++ b/WheelWizard.Test/Shared/Services/AvaloniaLoggerAdapterTests.cs @@ -0,0 +1,94 @@ +using Avalonia; +using Avalonia.Logging; +using Microsoft.Extensions.Logging; +using WheelWizard.Shared.Services; + +namespace WheelWizard.Test.Shared.Services; + +public class AvaloniaLoggerAdapterTests +{ + public static TheoryData LogLevels = + new() + { + { LogEventLevel.Verbose, LogLevel.Trace }, + { LogEventLevel.Debug, LogLevel.Debug }, + { LogEventLevel.Information, LogLevel.Information }, + { LogEventLevel.Warning, LogLevel.Warning }, + { LogEventLevel.Error, LogLevel.Error }, + { LogEventLevel.Fatal, LogLevel.Critical } + }; + + [Theory(DisplayName = "Log adapter message, should convert to logger message")] + [MemberData(nameof(LogLevels))] + public void LogAdapterMessage_ShouldConvertToLoggerMessage(LogEventLevel level, LogLevel expectedLevel) + { + // Arrange + var logger = Substitute.For>(); + var logAdapter = new AvaloniaLoggerAdapter(logger); + + var args = new object[] { "testValue" }; + + // Act + logAdapter.Log(level, "TestArea", null, "Test message {Arg}", args); + + // Assert + logger.Received(1).Log(expectedLevel, "Test message {Arg}", args); + } + + [Fact(DisplayName = "Log adapter without args, should log message without args")] + public void LogAdapterWithoutArgs_ShouldLogMessageWithoutArgs() + { + // Arrange + var logger = Substitute.For>(); + var logAdapter = new AvaloniaLoggerAdapter(logger); + + // Act + logAdapter.Log(LogEventLevel.Information, "TestArea", null, "Test message"); + + // Assert + logger.Received(1).Log(LogLevel.Information, "Test message"); + } + + [Fact(DisplayName = "Log adapter with layout area, should not log")] + public void LogAdapterWithLayoutArea_ShouldNotLog() + { + // Arrange + var logger = Substitute.For>(); + var logAdapter = new AvaloniaLoggerAdapter(logger); + + // Act + logAdapter.Log(LogEventLevel.Information, "Layout", null, "Test message"); + + // Assert +#pragma warning disable CA2254 + // ReSharper disable once TemplateIsNotCompileTimeConstantProblem + logger.DidNotReceive().Log(LogLevel.Information, "Test message"); +#pragma warning restore CA2254 + } + + [Fact(DisplayName = "Log adapter is enabled, should be true")] + public void LogAdapterIsEnabled_ShouldBeTrue() + { + // Arrange + var logger = Substitute.For>(); + var logAdapter = new AvaloniaLoggerAdapter(logger); + + // Act + var result = logAdapter.IsEnabled(LogEventLevel.Information, "TestArea"); + + // Assert + Assert.True(result); + } + + [Fact(DisplayName = "Invalid log event level log, should throw exception")] + public void InvalidLogEventLevelLog_ShouldThrowException() + { + // Arrange + var logger = Substitute.For>(); + var logAdapter = new AvaloniaLoggerAdapter(logger); + + // Act & Assert + Assert.Throws(() => + logAdapter.Log((LogEventLevel)999, "TestArea", null, "Test message")); + } +} diff --git a/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs b/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs index 568ff358..ce21c945 100644 --- a/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs +++ b/WheelWizard/Features/AutoUpdating/Platforms/LinuxUpdatePlatform.cs @@ -98,7 +98,7 @@ sleep 1 fileSystem.File.WriteAllText(scriptFilePath, scriptContent); // Ensure the script is executable. - var chmodResult = SafeExecute(() => + var chmodResult = TryCatch(() => Process.Start(new ProcessStartInfo { FileName = "/usr/bin/env", @@ -131,6 +131,6 @@ sleep 1 WorkingDirectory = currentFolder }; - return SafeExecute(() => Process.Start(processStartInfo), errorMessage: "Failed to execute the update script."); + return TryCatch(() => Process.Start(processStartInfo), errorMessage: "Failed to execute the update script."); } } diff --git a/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs b/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs index aa25319f..1538e356 100644 --- a/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs +++ b/WheelWizard/Features/AutoUpdating/Platforms/WindowsUpdatePlatform.cs @@ -44,7 +44,7 @@ private static OperationResult RestartAsAdmin() Verb = "runas" // This verb asks for elevation. }; - return SafeExecute(() => + return TryCatch(() => { Process.Start(startInfo); Environment.Exit(0); @@ -174,6 +174,6 @@ exit 1 WorkingDirectory = currentFolder }; - return SafeExecute(() => Process.Start(processStartInfo), errorMessage: "Failed to execute the update script."); + return TryCatch(() => Process.Start(processStartInfo), errorMessage: "Failed to execute the update script."); } } diff --git a/WheelWizard/Shared/OperationResult/OperationResult.cs b/WheelWizard/Shared/OperationResult/OperationResult.cs index 2e5b9442..09900474 100644 --- a/WheelWizard/Shared/OperationResult/OperationResult.cs +++ b/WheelWizard/Shared/OperationResult/OperationResult.cs @@ -82,7 +82,7 @@ public OperationResult(OperationError error) /// The error message to return if the function fails. /// The type of the value. /// An that indicates the result of the operation. - public static OperationResult SafeExecute(Func func, string? errorMessage = null) + public static OperationResult TryCatch(Func func, string? errorMessage = null) { try { @@ -107,7 +107,7 @@ public static OperationResult SafeExecute(Func func, string? errorMessa /// The error message to return if the function fails. /// The type of the value. /// An that indicates the result of the operation. - public static async Task> SafeExecute(Func> func, string? errorMessage = null) + public static async Task> TryCatch(Func> func, string? errorMessage = null) { try { @@ -131,7 +131,7 @@ public static async Task> SafeExecute(Func> func, /// The action to execute. /// The error message to return if the function fails. /// An that indicates the result of the operation. - public static OperationResult SafeExecute(Action action, string? errorMessage = null) + public static OperationResult TryCatch(Action action, string? errorMessage = null) { try { @@ -155,7 +155,7 @@ public static OperationResult SafeExecute(Action action, string? errorMessage = /// The action to execute. /// The error message to return if the function fails. /// An that indicates the result of the operation. - public static async Task SafeExecute(Func action, string? errorMessage = null) + public static async Task TryCatch(Func action, string? errorMessage = null) { try { diff --git a/WheelWizard/Shared/Services/ApiCaller.cs b/WheelWizard/Shared/Services/ApiCaller.cs index eeebb3e5..2a6890ee 100644 --- a/WheelWizard/Shared/Services/ApiCaller.cs +++ b/WheelWizard/Shared/Services/ApiCaller.cs @@ -32,7 +32,7 @@ public async Task> CallApiAsync(Expression(); - var result = await SafeExecute(async () => await apiCallFunction.Invoke(api), errorMessage: $"{apiName} call failed"); + var result = await TryCatch(async () => await apiCallFunction.Invoke(api), errorMessage: $"{apiName} call failed"); if (!result.IsSuccess) logger.LogError(result.Error.Exception, "API method '{ApiCall}' failed: {Message}", apiCallString, result.Error.Message);