From 88c98e878a7026ef7a8ae5e261566362655659e0 Mon Sep 17 00:00:00 2001 From: Joanna May Date: Tue, 9 Jan 2024 07:22:44 -0600 Subject: [PATCH] fix: better addons installation error reporting --- .vscode/settings.json | 4 +- .../install/AddonsInstallCommandTest.cs | 10 +-- .../AddonsLogic.State.CannotBeResolvedTest.cs | 23 ++---- ...nsLogic.State.InstallationSucceededTest.cs | 22 ++---- .../AddonsLogic.State.NothingToInstallTest.cs | 22 ++---- .../AddonsLogic.State.UnresolvedTest.cs | 78 +++++++------------ GodotEnv/Chickensoft.GodotEnv.csproj | 4 +- .../addons/commands/install/AddonsLogic.cs | 43 +++++----- 8 files changed, 77 insertions(+), 129 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e769113..97c8e45 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "[csharp]": { "editor.codeActionsOnSave": { "source.addMissingImports": "explicit", - "source.fixAll": "never", + "source.fixAll": "explicit", "source.organizeImports": "explicit" }, "editor.formatOnPaste": true, @@ -164,4 +164,4 @@ }, "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true, "dotnet.defaultSolution": "GodotEnv.sln" -} +} \ No newline at end of file diff --git a/GodotEnv.Tests/src/features/addons/commands/install/AddonsInstallCommandTest.cs b/GodotEnv.Tests/src/features/addons/commands/install/AddonsInstallCommandTest.cs index 01aa602..07d58ff 100644 --- a/GodotEnv.Tests/src/features/addons/commands/install/AddonsInstallCommandTest.cs +++ b/GodotEnv.Tests/src/features/addons/commands/install/AddonsInstallCommandTest.cs @@ -85,13 +85,11 @@ public async Task Executes() { logic.Setup(logic => logic.Bind()).Returns(binding.Object); - var logicContext = new Mock(); - logic .Setup(logic => logic.Input(It.IsAny())) .Returns( Task.FromResult( - new AddonsLogic.State.InstallationSucceeded(logicContext.Object) + new AddonsLogic.State.InstallationSucceeded() ) ); @@ -116,9 +114,8 @@ public void CheckSuccessThrowsOnStateCannotBeResolved() { var fileClient = new Mock(); var command = new AddonsInstallCommand(executionContext.Object); - var logicContext = new Mock(); - var state = new AddonsLogic.State.CannotBeResolved(logicContext.Object); + var state = new AddonsLogic.State.CannotBeResolved(); Assert.Throws( () => command.CheckSuccess(state) @@ -131,9 +128,8 @@ public void CheckSuccessThrowsOnStateUnresolved() { var fileClient = new Mock(); var command = new AddonsInstallCommand(executionContext.Object); - var logicContext = new Mock(); - var state = new AddonsLogic.State.Unresolved(logicContext.Object); + var state = new AddonsLogic.State.Unresolved(); Assert.Throws( () => command.CheckSuccess(state) diff --git a/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.CannotBeResolvedTest.cs b/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.CannotBeResolvedTest.cs index fd6abb1..7a00172 100644 --- a/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.CannotBeResolvedTest.cs +++ b/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.CannotBeResolvedTest.cs @@ -1,28 +1,19 @@ namespace Chickensoft.GodotEnv.Tests; -using Chickensoft.GodotEnv.Common.Utilities; +using System.Linq; using Chickensoft.GodotEnv.Features.Addons.Commands; -using Moq; +using Shouldly; using Xunit; public class CannotBeResolvedTest { [Fact] public void ReportsOnEnter() { - var context = new Mock(); - var log = new Mock(); + var state = new AddonsLogic.State.CannotBeResolved(); + var context = state.CreateFakeContext(); - log.Setup(log => log.Err(It.IsAny())); - context - .Setup(ctx => ctx.Output(It.IsAny())) - .Callback( - output => ((AddonsLogic.Output.Report)output).Event.Report(log.Object) - ); + state.Enter(); - var state = new AddonsLogic.State.CannotBeResolved(context.Object); - var stateTester = AddonsLogic.Test(state); - - stateTester.Enter(); - - context.VerifyAll(); + var output = + context.Outputs.Single().ShouldBeOfType(); } } diff --git a/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.InstallationSucceededTest.cs b/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.InstallationSucceededTest.cs index 51f55fc..4565685 100644 --- a/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.InstallationSucceededTest.cs +++ b/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.InstallationSucceededTest.cs @@ -1,28 +1,18 @@ namespace Chickensoft.GodotEnv.Tests; -using Chickensoft.GodotEnv.Common.Utilities; +using System.Linq; using Chickensoft.GodotEnv.Features.Addons.Commands; -using Moq; +using Shouldly; using Xunit; public class InstallationSucceededTest { [Fact] public void ReportsOnEnter() { - var context = new Mock(); - var log = new Mock(); + var state = new AddonsLogic.State.InstallationSucceeded(); + var context = state.CreateFakeContext(); - log.Setup(log => log.Err(It.IsAny())); - context - .Setup(ctx => ctx.Output(It.IsAny())) - .Callback( - output => ((AddonsLogic.Output.Report)output).Event.Report(log.Object) - ); + state.Enter(); - var state = new AddonsLogic.State.InstallationSucceeded(context.Object); - var stateTester = AddonsLogic.Test(state); - - stateTester.Enter(); - - context.VerifyAll(); + context.Outputs.Single().ShouldBeOfType(); } } diff --git a/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.NothingToInstallTest.cs b/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.NothingToInstallTest.cs index d3263d4..e0f8954 100644 --- a/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.NothingToInstallTest.cs +++ b/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.NothingToInstallTest.cs @@ -1,28 +1,18 @@ namespace Chickensoft.GodotEnv.Tests; -using Chickensoft.GodotEnv.Common.Utilities; +using System.Linq; using Chickensoft.GodotEnv.Features.Addons.Commands; -using Moq; +using Shouldly; using Xunit; public class NothingToInstallTest { [Fact] public void ReportsOnEnter() { - var context = new Mock(); - var log = new Mock(); + var state = new AddonsLogic.State.NothingToInstall(); + var context = state.CreateFakeContext(); - log.Setup(log => log.Err(It.IsAny())); - context - .Setup(ctx => ctx.Output(It.IsAny())) - .Callback( - output => ((AddonsLogic.Output.Report)output).Event.Report(log.Object) - ); + state.Enter(); - var state = new AddonsLogic.State.NothingToInstall(context.Object); - var stateTester = AddonsLogic.Test(state); - - stateTester.Enter(); - - context.VerifyAll(); + context.Outputs.Single().ShouldBeOfType(); } } diff --git a/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.UnresolvedTest.cs b/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.UnresolvedTest.cs index 258793f..c92b326 100644 --- a/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.UnresolvedTest.cs +++ b/GodotEnv.Tests/src/features/addons/commands/install/AddonsLogic.State.UnresolvedTest.cs @@ -17,8 +17,6 @@ public void CanGoOn() => [Fact] public async Task DoesNothingIfNothingToInstall() { - var context = new Mock(); - var addonsRepo = new Mock(); var addonsFileRepo = new Mock(); var addonGraph = new Mock(); @@ -28,15 +26,6 @@ public async Task DoesNothingIfNothingToInstall() { ProjectPath: projectPath, MaxDepth: null ); - context.Setup(context => context.Get()) - .Returns(addonsRepo.Object); - - context.Setup(context => context.Get()) - .Returns(addonsFileRepo.Object); - - context.Setup(context => context.Get()) - .Returns(addonGraph.Object); - var addonsFile = new AddonsFile( addons: new() { }, cacheRelativePath: ".addons", @@ -50,10 +39,15 @@ public async Task DoesNothingIfNothingToInstall() { projectPath, out addonsFilePath )).Returns(addonsFile); - var state = new AddonsLogic.State.Unresolved(context.Object); + var state = new AddonsLogic.State.Unresolved(); + var context = state.CreateFakeContext(); + + context.Set(addonsRepo.Object); + context.Set(addonsFileRepo.Object); + context.Set(addonGraph.Object); + var nextState = await state.On(input); - context.VerifyAll(); addonsRepo.VerifyAll(); addonsFileRepo.VerifyAll(); addonGraph.VerifyAll(); @@ -61,8 +55,6 @@ public async Task DoesNothingIfNothingToInstall() { [Fact] public async Task EndsInCannotBeResolvedIfFatalErrorEncountered() { - var context = new Mock(); - var addonsRepo = new Mock(); var addonsFileRepo = new Mock(); var addonGraph = new Mock(); @@ -72,15 +64,6 @@ public async Task EndsInCannotBeResolvedIfFatalErrorEncountered() { ProjectPath: projectPath, MaxDepth: null ); - context.Setup(context => context.Get()) - .Returns(addonsRepo.Object); - - context.Setup(context => context.Get()) - .Returns(addonsFileRepo.Object); - - context.Setup(context => context.Get()) - .Returns(addonGraph.Object); - var entry = new AddonsFileEntry( url: "https://github.com/chickensoft-games/addon" ); @@ -112,12 +95,16 @@ public async Task EndsInCannotBeResolvedIfFatalErrorEncountered() { addonGraph.Setup(graph => graph.Add(addon)) .Returns(graphResult); - var state = new AddonsLogic.State.Unresolved(context.Object); + var state = new AddonsLogic.State.Unresolved(); + var context = state.CreateFakeContext(); + context.Set(addonsRepo.Object); + context.Set(addonsFileRepo.Object); + context.Set(addonGraph.Object); + var nextState = await state.On(input); nextState.ShouldBeOfType(); - context.VerifyAll(); addonsRepo.VerifyAll(); addonsFileRepo.VerifyAll(); addonGraph.VerifyAll(); @@ -125,8 +112,6 @@ public async Task EndsInCannotBeResolvedIfFatalErrorEncountered() { [Fact] public async Task DeterminesCanonicalAddonCorrectly() { - var context = new Mock(); - var addonsRepo = new Mock(); var addonsFileRepo = new Mock(); var addonGraph = new Mock(); @@ -136,15 +121,6 @@ public async Task DeterminesCanonicalAddonCorrectly() { ProjectPath: projectPath, MaxDepth: null ); - context.Setup(context => context.Get()) - .Returns(addonsRepo.Object); - - context.Setup(context => context.Get()) - .Returns(addonsFileRepo.Object); - - context.Setup(context => context.Get()) - .Returns(addonGraph.Object); - var entry = new AddonsFileEntry( url: "https://github.com/chickensoft-games/addon" ); @@ -213,12 +189,17 @@ public async Task DeterminesCanonicalAddonCorrectly() { )) .Returns(otherAddonAddonsFile); - var state = new AddonsLogic.State.Unresolved(context.Object); + var state = new AddonsLogic.State.Unresolved(); + + var context = state.CreateFakeContext(); + context.Set(addonsRepo.Object); + context.Set(addonsFileRepo.Object); + context.Set(addonGraph.Object); + var nextState = await state.On(input); nextState.ShouldBeOfType(); - context.VerifyAll(); addonsRepo.VerifyAll(); addonsFileRepo.VerifyAll(); addonGraph.VerifyAll(); @@ -226,8 +207,6 @@ public async Task DeterminesCanonicalAddonCorrectly() { [Fact] public async Task InstallsSymlinkAddon() { - var context = new Mock(); - var addonsRepo = new Mock(); var addonsFileRepo = new Mock(); var addonGraph = new Mock(); @@ -237,15 +216,6 @@ public async Task InstallsSymlinkAddon() { ProjectPath: projectPath, MaxDepth: null ); - context.Setup(context => context.Get()) - .Returns(addonsRepo.Object); - - context.Setup(context => context.Get()) - .Returns(addonsFileRepo.Object); - - context.Setup(context => context.Get()) - .Returns(addonGraph.Object); - var entry = new AddonsFileEntry( url: "/symlink_addon/addon", source: AssetSource.Symlink @@ -297,12 +267,16 @@ public async Task InstallsSymlinkAddon() { addonsRepo .Setup(repo => repo.InstallAddonWithSymlink(It.IsAny())); - var state = new AddonsLogic.State.Unresolved(context.Object); + var state = new AddonsLogic.State.Unresolved(); + var context = state.CreateFakeContext(); + context.Set(addonsRepo.Object); + context.Set(addonsFileRepo.Object); + context.Set(addonGraph.Object); + var nextState = await state.On(input); nextState.ShouldBeOfType(); - context.VerifyAll(); addonsRepo.VerifyAll(); addonsFileRepo.VerifyAll(); addonGraph.VerifyAll(); diff --git a/GodotEnv/Chickensoft.GodotEnv.csproj b/GodotEnv/Chickensoft.GodotEnv.csproj index f938969..3501705 100644 --- a/GodotEnv/Chickensoft.GodotEnv.csproj +++ b/GodotEnv/Chickensoft.GodotEnv.csproj @@ -42,8 +42,8 @@ - - + + diff --git a/GodotEnv/src/features/addons/commands/install/AddonsLogic.cs b/GodotEnv/src/features/addons/commands/install/AddonsLogic.cs index 04dc2c9..b882565 100644 --- a/GodotEnv/src/features/addons/commands/install/AddonsLogic.cs +++ b/GodotEnv/src/features/addons/commands/install/AddonsLogic.cs @@ -1,5 +1,6 @@ namespace Chickensoft.GodotEnv.Features.Addons.Commands; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Chickensoft.GodotEnv.Common.Utilities; @@ -9,16 +10,13 @@ namespace Chickensoft.GodotEnv.Features.Addons.Commands; using Chickensoft.LogicBlocks.Generator; [StateMachine] -public partial class AddonsLogic : LogicBlockAsync { +public partial class AddonsLogic : LogicBlockAsync { public abstract record Input { - public record Install( - string ProjectPath, int? MaxDepth - ) : Input; + public readonly record struct Install(string ProjectPath, int? MaxDepth); } - public abstract record State(IContext Context) : StateLogic(Context) { - public record Unresolved(IContext Context) : - State(Context), IGet { + public abstract record State : StateLogic { + public record Unresolved : State, IGet { public async Task On(Input.Install input) { var addonsRepo = Context.Get(); var addonsFileRepo = Context.Get(); @@ -118,11 +116,11 @@ public async Task On(Input.Install input) { ); if (fatalResolutionError) { - return new CannotBeResolved(Context); + return new CannotBeResolved(); } if (addonsToInstall.Count == 0) { - return new NothingToInstall(Context); + return new NothingToInstall(); } // Install resolved addons. @@ -143,7 +141,7 @@ public async Task On(Input.Install input) { await addonsRepo.InstallAddonFromCache(addon, cacheName); } - return new InstallationSucceeded(Context); + return new InstallationSucceeded(); } /// @@ -165,7 +163,7 @@ maxDepth is null || depth < maxDepth } public record CannotBeResolved : State { - public CannotBeResolved(IContext context) : base(context) { + public CannotBeResolved() { OnEnter( (state) => { Context.Output( @@ -182,7 +180,7 @@ public CannotBeResolved(IContext context) : base(context) { } public record NothingToInstall : State { - public NothingToInstall(IContext context) : base(context) { + public NothingToInstall() { OnEnter( (state) => { Context.Output( @@ -199,7 +197,7 @@ public NothingToInstall(IContext context) : base(context) { } public record InstallationSucceeded : State { - public InstallationSucceeded(IContext context) : base(context) { + public InstallationSucceeded() { OnEnter( (state) => { Context.Output( @@ -217,12 +215,11 @@ public InstallationSucceeded(IContext context) : base(context) { } public abstract record Output { - public record Report(IReportableEvent Event) : Output; + public readonly record struct Report(IReportableEvent Event); } } -public partial class AddonsLogic : - LogicBlockAsync { +public partial class AddonsLogic : LogicBlockAsync { public AddonsLogic( IAddonsFileRepository addonsFileRepo, IAddonsRepository addonsRepo, @@ -233,6 +230,16 @@ IAddonGraph addonGraph Set(addonGraph); } - public override State GetInitialState(IContext context) => - new State.Unresolved(context); + public override State GetInitialState() => new State.Unresolved(); + + protected override void HandleError(Exception e) => Context.Output( + new Output.Report( + new ReportableEvent( + (log) => { + log.Err("An error occurred while installing addons:"); + log.Err(e.Message); + } + ) + ) + ); }