From f600e2b1a1ff7b76f26df5cf0c5c7543f7f951e8 Mon Sep 17 00:00:00 2001 From: Fredi Kats Date: Tue, 5 Sep 2023 23:53:11 +0400 Subject: [PATCH 1/4] Implement dotnet format execution in separated process --- .../CliExecution/CmdProcess.cs | 56 +++++++++++++++++++ .../CliExecution/CmdProcessException.cs | 12 ++++ .../DotnetFormat/DotnetFormatCli.cs | 24 ++++++++ .../Kysect.Configuin.Tests/CmdProcessTests.cs | 18 ++++++ .../DotnetFormat/DotnetFormatCliTests.cs | 29 ++++++++++ 5 files changed, 139 insertions(+) create mode 100644 Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs create mode 100644 Sources/Kysect.Configuin.Core/CliExecution/CmdProcessException.cs create mode 100644 Sources/Kysect.Configuin.Core/DotnetFormat/DotnetFormatCli.cs create mode 100644 Sources/Kysect.Configuin.Tests/CmdProcessTests.cs create mode 100644 Sources/Kysect.Configuin.Tests/DotnetFormat/DotnetFormatCliTests.cs diff --git a/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs b/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs new file mode 100644 index 0000000..5f9457c --- /dev/null +++ b/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs @@ -0,0 +1,56 @@ +using Kysect.CommonLib.BaseTypes.Extensions; +using System.Diagnostics; + +namespace Kysect.Configuin.Core.CliExecution; + +public class CmdProcess +{ + public async Task ExecuteCommand(string command) + { + var exceptions = new List(); + + using var process = new Process(); + + process.Exited += (sender, _) => + { + if (sender is null) + { + exceptions.Add(new NullReferenceException(nameof(sender))); + return; + } + + int exitCode = sender.To().ExitCode; + if (exitCode != 0) + { + exceptions.Add(new CmdProcessException($"Application return exit code {exitCode}.")); + } + }; + + process.ErrorDataReceived += (sender, args) => + { + if (args.Data is not null) + exceptions.Add(new CmdProcessException()); + }; + + var startInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + RedirectStandardError = true, + FileName = "cmd.exe", + Arguments = $"/C {command}" + }; + + process.StartInfo = startInfo; + process.Start(); + await process.WaitForExitAsync(); + + string errors = await process.StandardError.ReadToEndAsync(); + if (!string.IsNullOrEmpty(errors)) + exceptions.Add(new CmdProcessException(errors)); + + process.Close(); + + if (exceptions.Count > 0) + throw new AggregateException("Failed to execute cmd command", exceptions); + } +} \ No newline at end of file diff --git a/Sources/Kysect.Configuin.Core/CliExecution/CmdProcessException.cs b/Sources/Kysect.Configuin.Core/CliExecution/CmdProcessException.cs new file mode 100644 index 0000000..96c5dc6 --- /dev/null +++ b/Sources/Kysect.Configuin.Core/CliExecution/CmdProcessException.cs @@ -0,0 +1,12 @@ +namespace Kysect.Configuin.Core.CliExecution; + +public class CmdProcessException : Exception +{ + public CmdProcessException(string message) : base(message) + { + } + + public CmdProcessException() : base("Failed to execute cmd command") + { + } +} \ No newline at end of file diff --git a/Sources/Kysect.Configuin.Core/DotnetFormat/DotnetFormatCli.cs b/Sources/Kysect.Configuin.Core/DotnetFormat/DotnetFormatCli.cs new file mode 100644 index 0000000..9bd3766 --- /dev/null +++ b/Sources/Kysect.Configuin.Core/DotnetFormat/DotnetFormatCli.cs @@ -0,0 +1,24 @@ +using Kysect.Configuin.Core.CliExecution; + +namespace Kysect.Configuin.Core.DotnetFormat; + +public class DotnetFormatCli +{ + private readonly CmdProcess _cmdProcess = new CmdProcess(); + + public void Validate() + { + ExecuteCommand("dotnet format -h"); + } + + public void GenerateWarnings(string pathToSolution, string pathToJson) + { + + ExecuteCommand($"dotnet format \"{pathToSolution}\" --verify-no-changes --report \"{pathToJson}\""); + } + + private void ExecuteCommand(string command) + { + _cmdProcess.ExecuteCommand(command).Wait(); + } +} \ No newline at end of file diff --git a/Sources/Kysect.Configuin.Tests/CmdProcessTests.cs b/Sources/Kysect.Configuin.Tests/CmdProcessTests.cs new file mode 100644 index 0000000..9a4173d --- /dev/null +++ b/Sources/Kysect.Configuin.Tests/CmdProcessTests.cs @@ -0,0 +1,18 @@ +using Kysect.Configuin.Core.CliExecution; +using NUnit.Framework; + +namespace Kysect.Configuin.Tests; + +public class CmdProcessTests +{ + [Test] + public void Execute_ForInvalidCommand_ThrowError() + { + var cmdProcess = new CmdProcess(); + + Assert.Throws(() => + { + cmdProcess.ExecuteCommand("qwerasdf1234").Wait(); + }); + } +} \ No newline at end of file diff --git a/Sources/Kysect.Configuin.Tests/DotnetFormat/DotnetFormatCliTests.cs b/Sources/Kysect.Configuin.Tests/DotnetFormat/DotnetFormatCliTests.cs new file mode 100644 index 0000000..45f01b7 --- /dev/null +++ b/Sources/Kysect.Configuin.Tests/DotnetFormat/DotnetFormatCliTests.cs @@ -0,0 +1,29 @@ +using Kysect.Configuin.Core.DotnetFormat; +using NUnit.Framework; + +namespace Kysect.Configuin.Tests.DotnetFormat; + +public class DotnetFormatCliTests +{ + private readonly DotnetFormatCli _dotnetFormatCli; + + public DotnetFormatCliTests() + { + _dotnetFormatCli = new DotnetFormatCli(); + } + + [Test] + public void Validate_FinishedWithoutErrors() + { + _dotnetFormatCli.Validate(); + } + + [Test] + [Ignore("This test require infrastructure")] + public void GenerateWarnings_CreateReportFile() + { + const string pathToSln = "./../../../../"; + + _dotnetFormatCli.GenerateWarnings(pathToSln, "sample.json"); + } +} \ No newline at end of file From 7d059909904e63be8630c447db9bb0588bb54ce7 Mon Sep 17 00:00:00 2001 From: Fredi Kats Date: Wed, 6 Sep 2023 18:28:52 +0400 Subject: [PATCH 2/4] Introduce cmd execution result instead of exception throwing --- .../CliExecution/CmdExecutionResult.cs | 32 +++++++++++ .../CliExecution/CmdProcess.cs | 55 ++++++++----------- .../DotnetFormat/DotnetFormatCli.cs | 9 +-- .../Kysect.Configuin.Tests/CmdProcessTests.cs | 10 ++-- 4 files changed, 61 insertions(+), 45 deletions(-) create mode 100644 Sources/Kysect.Configuin.Core/CliExecution/CmdExecutionResult.cs diff --git a/Sources/Kysect.Configuin.Core/CliExecution/CmdExecutionResult.cs b/Sources/Kysect.Configuin.Core/CliExecution/CmdExecutionResult.cs new file mode 100644 index 0000000..45492fb --- /dev/null +++ b/Sources/Kysect.Configuin.Core/CliExecution/CmdExecutionResult.cs @@ -0,0 +1,32 @@ +namespace Kysect.Configuin.Core.CliExecution; + +public class CmdExecutionResult +{ + public int ExitCode { get; init; } + public IReadOnlyCollection Errors { get; init; } + + public CmdExecutionResult(int exitCode, IReadOnlyCollection errors) + { + ExitCode = exitCode; + Errors = errors; + } + + public void ThrowIfAnyError() + { + if (Errors.Count == 1) + throw new CmdProcessException(Errors.Single()); + + if (Errors.Count > 0) + { + var exceptions = Errors + .Select(m => new CmdProcessException(m)) + .ToList(); + throw new AggregateException(exceptions); + } + + if (ExitCode != 0) + { + throw new CmdProcessException($"Return {ExitCode} exit code."); + } + } +} \ No newline at end of file diff --git a/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs b/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs index 5f9457c..123dee1 100644 --- a/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs +++ b/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs @@ -1,37 +1,13 @@ -using Kysect.CommonLib.BaseTypes.Extensions; -using System.Diagnostics; +using System.Diagnostics; namespace Kysect.Configuin.Core.CliExecution; public class CmdProcess { - public async Task ExecuteCommand(string command) + public CmdExecutionResult ExecuteCommand(string command) { - var exceptions = new List(); - using var process = new Process(); - process.Exited += (sender, _) => - { - if (sender is null) - { - exceptions.Add(new NullReferenceException(nameof(sender))); - return; - } - - int exitCode = sender.To().ExitCode; - if (exitCode != 0) - { - exceptions.Add(new CmdProcessException($"Application return exit code {exitCode}.")); - } - }; - - process.ErrorDataReceived += (sender, args) => - { - if (args.Data is not null) - exceptions.Add(new CmdProcessException()); - }; - var startInfo = new ProcessStartInfo { WindowStyle = ProcessWindowStyle.Hidden, @@ -42,15 +18,28 @@ public async Task ExecuteCommand(string command) process.StartInfo = startInfo; process.Start(); - await process.WaitForExitAsync(); - - string errors = await process.StandardError.ReadToEndAsync(); - if (!string.IsNullOrEmpty(errors)) - exceptions.Add(new CmdProcessException(errors)); + process.WaitForExit(); + int exitCode = process.ExitCode; + IReadOnlyCollection errors = GetErrors(process); process.Close(); - if (exceptions.Count > 0) - throw new AggregateException("Failed to execute cmd command", exceptions); + return new CmdExecutionResult(exitCode, errors); + } + + private IReadOnlyCollection GetErrors(Process process) + { + var errors = new List(); + + // TODO: fixed error stream reading + // Line splitting triggered by char limit =_= + while (!process.StandardError.EndOfStream) + { + string? line = process.StandardError.ReadLine(); + if (line is not null) + errors.Add(line); + } + + return errors; } } \ No newline at end of file diff --git a/Sources/Kysect.Configuin.Core/DotnetFormat/DotnetFormatCli.cs b/Sources/Kysect.Configuin.Core/DotnetFormat/DotnetFormatCli.cs index 9bd3766..bd238c9 100644 --- a/Sources/Kysect.Configuin.Core/DotnetFormat/DotnetFormatCli.cs +++ b/Sources/Kysect.Configuin.Core/DotnetFormat/DotnetFormatCli.cs @@ -8,17 +8,12 @@ public class DotnetFormatCli public void Validate() { - ExecuteCommand("dotnet format -h"); + _cmdProcess.ExecuteCommand("dotnet format -h").ThrowIfAnyError(); } public void GenerateWarnings(string pathToSolution, string pathToJson) { - ExecuteCommand($"dotnet format \"{pathToSolution}\" --verify-no-changes --report \"{pathToJson}\""); - } - - private void ExecuteCommand(string command) - { - _cmdProcess.ExecuteCommand(command).Wait(); + _cmdProcess.ExecuteCommand($"dotnet format \"{pathToSolution}\" --verify-no-changes --report \"{pathToJson}\"").ThrowIfAnyError(); } } \ No newline at end of file diff --git a/Sources/Kysect.Configuin.Tests/CmdProcessTests.cs b/Sources/Kysect.Configuin.Tests/CmdProcessTests.cs index 9a4173d..5e54eca 100644 --- a/Sources/Kysect.Configuin.Tests/CmdProcessTests.cs +++ b/Sources/Kysect.Configuin.Tests/CmdProcessTests.cs @@ -1,4 +1,5 @@ -using Kysect.Configuin.Core.CliExecution; +using FluentAssertions; +using Kysect.Configuin.Core.CliExecution; using NUnit.Framework; namespace Kysect.Configuin.Tests; @@ -10,9 +11,8 @@ public void Execute_ForInvalidCommand_ThrowError() { var cmdProcess = new CmdProcess(); - Assert.Throws(() => - { - cmdProcess.ExecuteCommand("qwerasdf1234").Wait(); - }); + CmdExecutionResult cmdExecutionResult = cmdProcess.ExecuteCommand("qwerasdf1234"); + + cmdExecutionResult.ExitCode.Should().NotBe(0); } } \ No newline at end of file From 20f6328eed35b3e89603fe7a872c8b0c414b00d5 Mon Sep 17 00:00:00 2001 From: Fredi Kats Date: Wed, 6 Sep 2023 19:23:22 +0400 Subject: [PATCH 3/4] Add linux specific cmd for linux-like OS --- .../CliExecution/CmdProcess.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs b/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs index 123dee1..89e70d0 100644 --- a/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs +++ b/Sources/Kysect.Configuin.Core/CliExecution/CmdProcess.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Runtime.InteropServices; namespace Kysect.Configuin.Core.CliExecution; @@ -8,13 +9,7 @@ public CmdExecutionResult ExecuteCommand(string command) { using var process = new Process(); - var startInfo = new ProcessStartInfo - { - WindowStyle = ProcessWindowStyle.Hidden, - RedirectStandardError = true, - FileName = "cmd.exe", - Arguments = $"/C {command}" - }; + ProcessStartInfo startInfo = CreateProcessStartInfo(command); process.StartInfo = startInfo; process.Start(); @@ -27,6 +22,33 @@ public CmdExecutionResult ExecuteCommand(string command) return new CmdExecutionResult(exitCode, errors); } + private ProcessStartInfo CreateProcessStartInfo(string command) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + RedirectStandardError = true, + FileName = "cmd.exe", + Arguments = $"/C {command}" + }; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + RedirectStandardError = true, + FileName = "sh", + Arguments = $"-c {command}" + }; + } + + throw new NotSupportedException(RuntimeInformation.OSDescription); + } + private IReadOnlyCollection GetErrors(Process process) { var errors = new List(); From 30d8d3712961a9d93fd504a9e38488893cb97c6b Mon Sep 17 00:00:00 2001 From: Fredi Kats Date: Wed, 6 Sep 2023 20:36:35 +0400 Subject: [PATCH 4/4] Ignore test that require dotnet-format installing --- .../Kysect.Configuin.Tests/DotnetFormat/DotnetFormatCliTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Kysect.Configuin.Tests/DotnetFormat/DotnetFormatCliTests.cs b/Sources/Kysect.Configuin.Tests/DotnetFormat/DotnetFormatCliTests.cs index 45f01b7..5ef4184 100644 --- a/Sources/Kysect.Configuin.Tests/DotnetFormat/DotnetFormatCliTests.cs +++ b/Sources/Kysect.Configuin.Tests/DotnetFormat/DotnetFormatCliTests.cs @@ -13,6 +13,7 @@ public DotnetFormatCliTests() } [Test] + [Ignore("This test require infrastructure")] public void Validate_FinishedWithoutErrors() { _dotnetFormatCli.Validate();