From 9d5f25266de4f8bc4a5062aeb16487d982279374 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Mon, 1 Aug 2022 13:55:33 +0430 Subject: [PATCH] fix: cached csx not throwing error with non zero exit code #49 --- src/Husky/Cli/CommandBase.cs | 1 - src/Husky/Cli/ExecCommand.cs | 25 ++++++++--- src/Husky/Program.cs | 2 +- src/Husky/Services/AssemblyProxy.cs | 12 +++++ src/Husky/Services/Contracts/IAssembly.cs | 8 ++++ tests/HuskyTest/Cli/ExecCommandTests.cs | 55 ++++++++++++++++++++--- 6 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 src/Husky/Services/AssemblyProxy.cs create mode 100644 src/Husky/Services/Contracts/IAssembly.cs diff --git a/src/Husky/Cli/CommandBase.cs b/src/Husky/Cli/CommandBase.cs index 08ae5b4..6285ee7 100644 --- a/src/Husky/Cli/CommandBase.cs +++ b/src/Husky/Cli/CommandBase.cs @@ -1,4 +1,3 @@ -using CliFx; using CliFx.Attributes; using CliFx.Exceptions; using CliFx.Infrastructure; diff --git a/src/Husky/Cli/ExecCommand.cs b/src/Husky/Cli/ExecCommand.cs index 8d1f296..897510a 100644 --- a/src/Husky/Cli/ExecCommand.cs +++ b/src/Husky/Cli/ExecCommand.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using System.IO.Abstractions; -using System.Reflection; using System.Security.Cryptography; using CliFx.Attributes; using CliFx.Exceptions; @@ -19,11 +18,13 @@ public class ExecCommand : CommandBase { private readonly IFileSystem _fileSystem; private readonly IGit _git; + private readonly IAssembly _assembly; - public ExecCommand(IFileSystem fileSystem, IGit git) + public ExecCommand(IFileSystem fileSystem, IGit git, IAssembly assembly) { _fileSystem = fileSystem; _git = git; + _assembly = assembly; } [CommandParameter(0, Description = "The script file to execute")] @@ -97,8 +98,9 @@ private async Task GenerateScriptCache(string scriptPath, string compiledScriptP internal async Task ExecuteCachedScript(string scriptPath) { + var gitPath = await _git.GetGitPathAsync(); - var assembly = Assembly.LoadFile(_fileSystem.Path.Combine(gitPath, scriptPath)); + var assembly = _assembly.LoadFile(_fileSystem.Path.Combine(gitPath, scriptPath)); var type = assembly.GetType("Submission#0"); if (type == null) { @@ -117,7 +119,20 @@ internal async Task ExecuteCachedScript(string scriptPath) if (factory.Invoke(null, new object[] { submissionArray }) is Task task) try { - await task; + var result = await task; + switch (result) + { + case null or 0: + return; + case int i: + throw new CommandException("script execution failed", i); + default: + throw new CommandException("script execution failed"); + } + } + catch (CommandException) + { + throw; } catch (Exception e) { @@ -142,7 +157,7 @@ internal async ValueTask ExecuteScript(string scriptPath) internal async Task<(bool, string)> GetCachedScript(string scriptPath) { var cacheFolder = await GetHuskyCacheFolder(); - await using var fileStream = new FileStream(scriptPath, FileMode.Open); + await using var fileStream = _fileSystem.FileStream.Create(scriptPath, FileMode.Open); var hash = await CalculateHashAsync(fileStream); diff --git a/src/Husky/Program.cs b/src/Husky/Program.cs index 6083fbd..aed1829 100644 --- a/src/Husky/Program.cs +++ b/src/Husky/Program.cs @@ -56,10 +56,10 @@ ServiceProvider BuildServiceProvider() .AddSingleton() .AddSingleton() .AddSingleton() - .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() // Commands diff --git a/src/Husky/Services/AssemblyProxy.cs b/src/Husky/Services/AssemblyProxy.cs new file mode 100644 index 0000000..d015f79 --- /dev/null +++ b/src/Husky/Services/AssemblyProxy.cs @@ -0,0 +1,12 @@ +using System.Reflection; +using Husky.Services.Contracts; + +namespace Husky.Services; + +public class AssemblyProxy : IAssembly +{ + public Assembly LoadFile(string path) + { + return Assembly.LoadFile(path); + } +} diff --git a/src/Husky/Services/Contracts/IAssembly.cs b/src/Husky/Services/Contracts/IAssembly.cs new file mode 100644 index 0000000..094da03 --- /dev/null +++ b/src/Husky/Services/Contracts/IAssembly.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace Husky.Services.Contracts; + +public interface IAssembly +{ + public Assembly LoadFile(string path); +} diff --git a/tests/HuskyTest/Cli/ExecCommandTests.cs b/tests/HuskyTest/Cli/ExecCommandTests.cs index 0ae6861..ca3a3d9 100644 --- a/tests/HuskyTest/Cli/ExecCommandTests.cs +++ b/tests/HuskyTest/Cli/ExecCommandTests.cs @@ -1,10 +1,17 @@ +using System.Collections.Immutable; using System.IO.Abstractions; +using System.Reflection; +using System.Text; using CliFx.Exceptions; using CliFx.Infrastructure; using FluentAssertions; using Husky.Cli; using Husky.Services.Contracts; using Husky.Stdout; +using Husky.Utils; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; using NSubstitute; using Xunit; @@ -15,6 +22,7 @@ public class ExecCommandTests private readonly FakeInMemoryConsole _console; private readonly IFileSystem _io; private readonly IGit _git; + private readonly IAssembly _assembly; public ExecCommandTests() { @@ -24,6 +32,7 @@ public ExecCommandTests() // sub _io = Substitute.For(); _git = Substitute.For(); + _assembly = Substitute.For(); } [Fact] @@ -31,7 +40,7 @@ public async Task Exec_WithoutCache_WhenFileMissing_ThrowException() { // Arrange const string filePath = "fake_file.csx"; - var command = new ExecCommand(_io, _git) { Path = filePath, NoCache = true }; + var command = new ExecCommand(_io, _git, _assembly) { Path = filePath, NoCache = true }; // Act Func act = async () => await command.ExecuteAsync(_console); @@ -48,7 +57,7 @@ public async Task Exec_WithoutCache_WithErrorInScript_ThrowException() _io.File.Exists(Arg.Any()).Returns(true); _io.Path.GetDirectoryName(Arg.Any()).Returns(Directory.GetCurrentDirectory()); _io.File.ReadAllTextAsync(Arg.Any()).Returns(stringContent); - var command = new ExecCommand(_io, _git) { Path = filePath, NoCache = true }; + var command = new ExecCommand(_io, _git, _assembly) { Path = filePath, NoCache = true }; // Act Func act = async () => await command.ExecuteAsync(_console); @@ -68,7 +77,7 @@ public async Task Exec_WithoutCache_WithScriptThrowException_ThrowException() _io.Path.GetDirectoryName(Arg.Any()).Returns(Directory.GetCurrentDirectory()); _io.File.Exists(Arg.Any()).Returns(true); _io.File.ReadAllTextAsync(Arg.Any()).Returns(stringContent); - var command = new ExecCommand(_io, _git) { Path = filePath, NoCache = true }; + var command = new ExecCommand(_io, _git, _assembly) { Path = filePath, NoCache = true }; // Act Func act = async () => await command.ExecuteAsync(_console); @@ -87,7 +96,7 @@ public async Task Exec_WithoutCache_WithScriptReturnGreaterThan0_ThrowException( _io.Path.GetDirectoryName(Arg.Any()).Returns(Directory.GetCurrentDirectory()); _io.File.Exists(Arg.Any()).Returns(true); _io.File.ReadAllTextAsync(Arg.Any()).Returns(stringContent); - var command = new ExecCommand(_io, _git) { Path = filePath, NoCache = true }; + var command = new ExecCommand(_io, _git, _assembly) { Path = filePath, NoCache = true }; // Act Func act = async () => await command.ExecuteAsync(_console); @@ -106,7 +115,7 @@ public async Task Exec_WithoutCache_WithoutArguments_Succeed() _io.Path.GetDirectoryName(Arg.Any()).Returns(Directory.GetCurrentDirectory()); _io.File.Exists(Arg.Any()).Returns(true); _io.File.ReadAllTextAsync(Arg.Any()).Returns(stringContent); - var command = new ExecCommand(_io, _git) { Path = filePath, NoCache = true }; + var command = new ExecCommand(_io, _git, _assembly) { Path = filePath, NoCache = true }; // Act await command.ExecuteAsync(_console); @@ -123,10 +132,44 @@ public async Task Exec_WithoutCache_WithArguments_Succeed() _io.Path.GetDirectoryName(Arg.Any()).Returns(Directory.GetCurrentDirectory()); _io.File.Exists(Arg.Any()).Returns(true); _io.File.ReadAllTextAsync(Arg.Any()).Returns(stringContent); - var command = new ExecCommand(_io, _git) { Path = filePath, Arguments = new List { "test" }, NoCache = true }; + var command = new ExecCommand(_io, _git, _assembly) { Path = filePath, Arguments = new List { "test" }, NoCache = true }; // Act await command.ExecuteAsync(_console); } + + [Fact] + public async Task Exec_CachedScript_ShouldHandleNonZeroExitCode() + { + // Arrange + const string filePath = "fake_file.csx"; + const string stringContent = @" + return 1; + "; + _io.Path.GetDirectoryName(Arg.Any()).Returns(Directory.GetCurrentDirectory()); + _io.File.Exists(Arg.Any()).Returns(true); + _io.Directory.Exists(Arg.Any()).Returns(true); + _io.File.ReadAllTextAsync(Arg.Any()).Returns(stringContent); + var stream = new MemoryStream(Encoding.UTF8.GetBytes(stringContent)); + _io.FileStream.Create(Arg.Any(), FileMode.Open).Returns(stream); + + var opts = ScriptOptions.Default + .WithSourceResolver(new SourceFileResolver(ImmutableArray.Empty, null)) + .WithImports("System", "System.IO", "System.Collections.Generic", "System.Text", "System.Threading.Tasks"); + var script = CSharpScript.Create(stringContent.Trim(), opts, typeof(Globals)); + var compilation = script.GetCompilation(); + + await using var assemblyStream = new MemoryStream(); + var result = compilation.Emit(assemblyStream); + _assembly.LoadFile(Arg.Any()).Returns(Assembly.Load(assemblyStream.ToArray())); + + var command = new ExecCommand(_io, _git, _assembly) { Path = filePath, Arguments = new List { "test" }, NoCache = false }; + + // Act + Func act = async () => await command.ExecuteAsync(_console); + + // Assert + await act.Should().ThrowAsync().WithMessage("script execution failed"); + } } }