diff --git a/.vscode/launch.json b/.vscode/launch.json index ba1171e..ea9820e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ //"compare", "-f", "console", "-b", "../tests/test-data/report-02/Benchmark-report-full.json", "-t", "../tests/test-data/report-01/Benchmark-report-full.json", "-tm", "4%", "-ta", "5%" //"compare", "-f", "console", "-b", "../tests/test-data/report-01/Benchmark-report-full.json", "-t", "../tests/test-data/report-04/Benchmark-report-full.json" //"compare", "-f", "markdown", "-b", "../tests/test-data/report-01/Benchmark-report-full.json", "-t", "../tests/test-data/report-02/Benchmark-report-full.json", "-tm", "12ns", "-ta", "5%" - "compare", "-f", "console", "-f", "markdown", "-b", "../tests/test-data/report-12", "-t", "../tests/test-data/report-11" + "compare", "-f", "CONSOLE", "-f", "markdown", "-b", "../tests/test-data/report-12", "-t", "../tests/test-data/report-11" ], "cwd": "${workspaceFolder}/src", "console": "internalConsole", diff --git a/src/Commands/ComparerCommand.cs b/src/Commands/ComparerCommand.cs index 650f34e..02fd318 100644 --- a/src/Commands/ComparerCommand.cs +++ b/src/Commands/ComparerCommand.cs @@ -11,7 +11,7 @@ namespace PowerUtils.BenchmarkDotnet.Reporter.Commands; public interface IComparerCommand { - void Execute(string? baseline, string? target, string? meanThreshold, string? allocationThreshold, string[] formats, string output); + int Execute(string? baseline, string? target, string? meanThreshold, string? allocationThreshold, string[] formats, string output); } public sealed class ComparerCommand( @@ -24,7 +24,7 @@ public sealed class ComparerCommand( private readonly IServiceProvider _provider = provider; - public void Execute( + public int Execute( string? baseline, string? target, string? meanThreshold, @@ -166,5 +166,8 @@ public void Execute( .GetRequiredKeyedService(format.ToLower()) .Generate(comparerReport, output); } + + + return 0; // Success exit code } } diff --git a/src/Options/AllocationThresholdOption.cs b/src/Options/AllocationThresholdOption.cs new file mode 100644 index 0000000..ae753aa --- /dev/null +++ b/src/Options/AllocationThresholdOption.cs @@ -0,0 +1,12 @@ +using System.CommandLine; + +namespace PowerUtils.BenchmarkDotnet.Reporter.Options; + +public sealed class AllocationThresholdOption : Option +{ + public AllocationThresholdOption() + : base("--threshold-allocation", "-ta") + { + Description = "Throw an error when the allocation threshold is met. Examples: 5%, 10b, 10kb, 100mb, 1gb."; + } +} diff --git a/src/Options/BaselineOption.cs b/src/Options/BaselineOption.cs new file mode 100644 index 0000000..a4e721f --- /dev/null +++ b/src/Options/BaselineOption.cs @@ -0,0 +1,13 @@ +using System.CommandLine; + +namespace PowerUtils.BenchmarkDotnet.Reporter.Options; + +public sealed class BaselineOption : Option +{ + public BaselineOption() + : base("--baseline", "-b") + { + Description = "Path to the folder or file with Baseline report."; + Required = true; + } +} diff --git a/src/Options/FormatsOption.cs b/src/Options/FormatsOption.cs new file mode 100644 index 0000000..3d09ddb --- /dev/null +++ b/src/Options/FormatsOption.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.Linq; + +namespace PowerUtils.BenchmarkDotnet.Reporter.Options; + +public sealed class FormatsOption : Option +{ + private static readonly HashSet _allowedValues = new(StringComparer.OrdinalIgnoreCase) { "console", "markdown", "json", "hit-txt" }; + + public FormatsOption() + : base("--format", "-f") + { + Description = "Output format for the report."; + DefaultValueFactory = _ => ["console"]; + + Validators.Add(static result => + { + var values = result.Tokens + .Select(token => token.Value); + foreach(var value in values) + { + if(!_allowedValues.Contains(value)) + { + result.AddError($"Invalid format '{value}'. Allowed values: {string.Join(", ", _allowedValues)}"); + } + } + }); + } +} diff --git a/src/Options/MeanThresholdOption.cs b/src/Options/MeanThresholdOption.cs new file mode 100644 index 0000000..f7a6216 --- /dev/null +++ b/src/Options/MeanThresholdOption.cs @@ -0,0 +1,12 @@ +using System.CommandLine; + +namespace PowerUtils.BenchmarkDotnet.Reporter.Options; + +public sealed class MeanThresholdOption : Option +{ + public MeanThresholdOption() + : base("--threshold-mean", "-tm") + { + Description = "Throw an error when the mean threshold is met. Examples: 5%, 10ms, 10μs, 100ns, 1s."; + } +} diff --git a/src/Options/OutputOption.cs b/src/Options/OutputOption.cs new file mode 100644 index 0000000..3f0ab39 --- /dev/null +++ b/src/Options/OutputOption.cs @@ -0,0 +1,13 @@ +using System.CommandLine; + +namespace PowerUtils.BenchmarkDotnet.Reporter.Options; + +public sealed class OutputOption : Option +{ + public OutputOption() + : base("--output", "-o") + { + Description = "Output directory to export the diff report. Default is current directory."; + DefaultValueFactory = _ => "./BenchmarkReporter"; + } +} diff --git a/src/Options/TargetOption.cs b/src/Options/TargetOption.cs new file mode 100644 index 0000000..3c24ff9 --- /dev/null +++ b/src/Options/TargetOption.cs @@ -0,0 +1,13 @@ +using System.CommandLine; + +namespace PowerUtils.BenchmarkDotnet.Reporter.Options; + +public sealed class TargetOption : Option +{ + public TargetOption() + : base("--target", "-t") + { + Description = "Path to the folder or file with target reports."; + Required = true; + } +} diff --git a/src/PowerUtils.BenchmarkDotnet.Reporter.csproj b/src/PowerUtils.BenchmarkDotnet.Reporter.csproj index 9d1a77a..aa64d3a 100644 --- a/src/PowerUtils.BenchmarkDotnet.Reporter.csproj +++ b/src/PowerUtils.BenchmarkDotnet.Reporter.csproj @@ -95,7 +95,7 @@ - + diff --git a/src/Program.cs b/src/Program.cs index 2a455c2..9f35499 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,5 +1,4 @@ using System; -using System.CommandLine; using System.Globalization; using System.Threading; using Microsoft.Extensions.DependencyInjection; @@ -15,6 +14,7 @@ var serviceCollection = new ServiceCollection(); serviceCollection + .AddTransient() .AddTransient(sp => (message) => IOHelpers.Print(message)) .AddTransient(sp => @@ -28,5 +28,7 @@ .AddKeyedTransient("console") .AddTransient(); -var tool = new ToolCommands(serviceCollection.BuildServiceProvider()); -tool.Invoke(args); +return serviceCollection.BuildServiceProvider() + .GetRequiredService() + .Parse(args) + .Invoke(); diff --git a/src/ToolCommands.cs b/src/ToolCommands.cs index 3893fd5..94dd4aa 100644 --- a/src/ToolCommands.cs +++ b/src/ToolCommands.cs @@ -2,6 +2,7 @@ using System.CommandLine; using Microsoft.Extensions.DependencyInjection; using PowerUtils.BenchmarkDotnet.Reporter.Commands; +using PowerUtils.BenchmarkDotnet.Reporter.Options; namespace PowerUtils.BenchmarkDotnet.Reporter; @@ -9,14 +10,14 @@ public sealed class ToolCommands : RootCommand { public ToolCommands(IServiceProvider provider) { - var baseline = _createBaselineOption(); - var target = _createTargetOption(); + var baseline = new BaselineOption(); + var target = new TargetOption(); - var meanThreshold = _createMeanThresholdOption(); - var allocationThreshold = _createAllocationThresholdOption(); + var meanThreshold = new MeanThresholdOption(); + var allocationThreshold = new AllocationThresholdOption(); - var formats = _createFormatsOption(); - var output = _createOutputOption(); + var formats = new FormatsOption(); + var output = new OutputOption(); var compareCommand = new Command( "compare", @@ -30,60 +31,16 @@ public ToolCommands(IServiceProvider provider) output }; - compareCommand - .SetHandler( - provider.GetRequiredService().Execute, - baseline, - target, - meanThreshold, - allocationThreshold, - formats, - output); - - Add(compareCommand); + compareCommand.SetAction(parser => provider + .GetRequiredService() + .Execute( + parser.GetValue(baseline)!, + parser.GetValue(target)!, + parser.GetValue(meanThreshold), + parser.GetValue(allocationThreshold), + parser.GetValue(formats)!, + parser.GetValue(output)!)); + + Subcommands.Add(compareCommand); } - - - private static Option _createBaselineOption() - => new( - ["-b", "--baseline"], - "Path to the folder or file with Baseline report.") - { - IsRequired = true - }; - - private static Option _createTargetOption() - => new( - ["-t", "--target"], - "Path to the folder or file with target reports.") - { - IsRequired = true - }; - - private static Option _createMeanThresholdOption() - => new( - ["-tm", "--threshold-mean"], - "Throw an error when the mean threshold is met. Examples: 5%, 10ms, 10μs, 100ns, 1s."); - - private static Option _createAllocationThresholdOption() - => new( - ["-ta", "--threshold-allocation"], - "Throw an error when the allocation threshold is met. Examples: 5%, 10b, 10kb, 100mb, 1gb."); - - private static Option _createFormatsOption() - => new Option( - ["-f", "--format"], - () => ["console"], - "Output format for the report.") - .FromAmong( - "console", - "markdown", - "json", - "hit-txt"); - - private static Option _createOutputOption() - => new( - ["-o", "--output"], - () => "./BenchmarkReporter", - "Output directory to export the diff report. Default is current directory."); } diff --git a/tests/PowerUtils.BenchmarkDotnet.Reporter.Tests/Options/FormatOptionTests.cs b/tests/PowerUtils.BenchmarkDotnet.Reporter.Tests/Options/FormatOptionTests.cs new file mode 100644 index 0000000..6823546 --- /dev/null +++ b/tests/PowerUtils.BenchmarkDotnet.Reporter.Tests/Options/FormatOptionTests.cs @@ -0,0 +1,113 @@ +using System; +using System.Linq; +using PowerUtils.BenchmarkDotnet.Reporter.Commands; + +namespace PowerUtils.BenchmarkDotnet.Reporter.Tests.Options; + +public sealed class FormatOptionTests +{ + private readonly IServiceProvider _provider; + + public FormatOptionTests() + { + _provider = Substitute.For(); + var command = Substitute.For(); + + _provider + .GetService(typeof(IComparerCommand)) + .Returns(command); + } + + [Fact] + public void CompareCommand_ShouldHave_FormatsOption() + { + // Arrange & Act + var toolCommands = new ToolCommands(_provider); + + + // Assert + var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); + var formatsOption = compareCommand.Options.Single(o => o.Name == "--format"); + + formatsOption.ValueType.ShouldBe(typeof(string[])); + formatsOption.Aliases.Count.ShouldBe(1); + formatsOption.Aliases.ShouldContain("-f"); + formatsOption.Description.ShouldBe("Output format for the report."); + (formatsOption.GetDefaultValue() as string[]).ShouldBe(["console"]); + } + + [Theory] + [InlineData("markdown")] + [InlineData("jSOn")] + [InlineData("HIT-TXT")] + [InlineData("console")] + public void When_Format_Is_Valid_Shouldnt_Have_Validation_Error(string format) + { + // Arrange + var command = "compare"; + var option = "--format"; + + var toolCommands = new ToolCommands(_provider); + var compareCommand = toolCommands.Subcommands.Single(c => c.Name == command); + var formatsOption = compareCommand.Options.Single(o => o.Name == option); + var validation = formatsOption.Validators.Single(); + + + // Act + var parseResult = toolCommands.Parse($"{command} {option} {format}"); + var firstOptionResult = parseResult.GetResult(formatsOption); + + // Assert + firstOptionResult?.Errors.Count().ShouldBe(0); + } + + [Theory] + [InlineData("invalid-format")] + [InlineData("csv")] + [InlineData("html")] + public void When_Format_Is_Invalid_Should_Have_Validation_Error(string format) + { + // Arrange + var command = "compare"; + var option = "--format"; + + var toolCommands = new ToolCommands(_provider); + var compareCommand = toolCommands.Subcommands.Single(c => c.Name == command); + var formatsOption = compareCommand.Options.Single(o => o.Name == option); + var validation = formatsOption.Validators.Single(); + + + // Act + var parseResult = toolCommands.Parse($"{command} {option} {format}"); + var firstOptionResult = parseResult.GetResult(formatsOption); + + // Assert + firstOptionResult?.Errors.Count().ShouldBe(1); + firstOptionResult?.Errors.ShouldContain(e => e.Message == $"Invalid format '{format}'. Allowed values: console, markdown, json, hit-txt"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void When_Format_Isnt_Defined_Should_Have_Validation_Error(string? format) + { + // Arrange + var command = "compare"; + var option = "--format"; + + var toolCommands = new ToolCommands(_provider); + var compareCommand = toolCommands.Subcommands.Single(c => c.Name == command); + var formatsOption = compareCommand.Options.Single(o => o.Name == option); + var validation = formatsOption.Validators.Single(); + + + // Act + var parseResult = toolCommands.Parse($"{command} {option} {format}"); + var firstOptionResult = parseResult.GetResult(formatsOption); + + // Assert + firstOptionResult?.Errors.Count().ShouldBe(1); + firstOptionResult?.Errors.ShouldContain(e => e.Message == "Required argument missing for option: '--format'."); + } +} diff --git a/tests/PowerUtils.BenchmarkDotnet.Reporter.Tests/Options/OptionsTests.cs b/tests/PowerUtils.BenchmarkDotnet.Reporter.Tests/Options/OptionsTests.cs new file mode 100644 index 0000000..2e689fb --- /dev/null +++ b/tests/PowerUtils.BenchmarkDotnet.Reporter.Tests/Options/OptionsTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using PowerUtils.BenchmarkDotnet.Reporter.Commands; + +namespace PowerUtils.BenchmarkDotnet.Reporter.Tests.Options; + +public sealed class OptionsTests +{ + private readonly IServiceProvider _provider; + + public OptionsTests() + { + _provider = Substitute.For(); + var command = Substitute.For(); + + _provider + .GetService(typeof(IComparerCommand)) + .Returns(command); + } + + [Fact] + public void CompareCommand_ShouldHave_BaselineOption() + { + // Arrange & Act + var toolCommands = new ToolCommands(_provider); + + + // Assert + var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); + var baselineOption = compareCommand.Options.Single(o => o.Name == "--baseline"); + + baselineOption.ValueType.ShouldBe(typeof(string)); + baselineOption.Aliases.Count.ShouldBe(1); + baselineOption.Aliases.ShouldContain("-b"); + baselineOption.Required.ShouldBeTrue(); + baselineOption.Description.ShouldBe("Path to the folder or file with Baseline report."); + } + + [Fact] + public void CompareCommand_ShouldHave_TargetOption() + { + // Arrange & Act + var toolCommands = new ToolCommands(_provider); + + + // Assert + var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); + var targetOption = compareCommand.Options.Single(o => o.Name == "--target"); + + targetOption.ValueType.ShouldBe(typeof(string)); + targetOption.Aliases.Count.ShouldBe(1); + targetOption.Aliases.ShouldContain("-t"); + targetOption.Required.ShouldBeTrue(); + targetOption.Description.ShouldBe("Path to the folder or file with target reports."); + } + + [Fact] + public void CompareCommand_ShouldHave_ThresholdMeanOption() + { + // Arrange & Act + var toolCommands = new ToolCommands(_provider); + + + // Assert + var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); + var meanThresholdOption = compareCommand.Options.Single(o => o.Name == "--threshold-mean"); + + meanThresholdOption.ValueType.ShouldBe(typeof(string)); + meanThresholdOption.Aliases.Count.ShouldBe(1); + meanThresholdOption.Aliases.ShouldContain("-tm"); + meanThresholdOption.Description.ShouldBe("Throw an error when the mean threshold is met. Examples: 5%, 10ms, 10μs, 100ns, 1s."); + } + + [Fact] + public void CompareCommand_ShouldHave_ThresholdAllocationOption() + { + // Arrange & Act + var toolCommands = new ToolCommands(_provider); + + + // Assert + var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); + var allocationThresholdOption = compareCommand.Options.Single(o => o.Name == "--threshold-allocation"); + + allocationThresholdOption.ValueType.ShouldBe(typeof(string)); + allocationThresholdOption.Aliases.Count.ShouldBe(1); + allocationThresholdOption.Aliases.ShouldContain("-ta"); + allocationThresholdOption.Description.ShouldBe("Throw an error when the allocation threshold is met. Examples: 5%, 10b, 10kb, 100mb, 1gb."); + } + + [Fact] + public void CompareCommand_ShouldHave_OutputOption() + { + // Arrange & Act + var toolCommands = new ToolCommands(_provider); + + + // Assert + var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); + var outputOption = compareCommand.Options.Single(o => o.Name == "--output"); + + outputOption.ValueType.ShouldBe(typeof(string)); + outputOption.Aliases.Count.ShouldBe(1); + outputOption.Aliases.ShouldContain("-o"); + outputOption.Description.ShouldBe("Output directory to export the diff report. Default is current directory."); + (outputOption.GetDefaultValue() as string).ShouldBe("./BenchmarkReporter"); + } +} diff --git a/tests/PowerUtils.BenchmarkDotnet.Reporter.Tests/ToolCommandsTest.cs b/tests/PowerUtils.BenchmarkDotnet.Reporter.Tests/ToolCommandsTest.cs index 0d50c7c..c78710d 100644 --- a/tests/PowerUtils.BenchmarkDotnet.Reporter.Tests/ToolCommandsTest.cs +++ b/tests/PowerUtils.BenchmarkDotnet.Reporter.Tests/ToolCommandsTest.cs @@ -45,7 +45,7 @@ public void RootCommands_ShouldContain_CompareCommand() } [Fact] - public void CompareCommand_ShouldHave_4Options() + public void CompareCommand_ShouldHave_6Options() { // Arrange & Act var toolCommands = new ToolCommands(_provider); @@ -56,108 +56,4 @@ public void CompareCommand_ShouldHave_4Options() compareCommand.Options.Count.ShouldBe(6); } - - [Fact] - public void CompareCommand_ShouldHave_BaselineOption() - { - // Arrange & Act - var toolCommands = new ToolCommands(_provider); - - - // Assert - var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); - var baselineOption = compareCommand.Options.Single(o => o.Name == "baseline"); - - baselineOption.ValueType.ShouldBe(typeof(string)); - baselineOption.Aliases.ShouldContain("-b"); - baselineOption.Aliases.ShouldContain("--baseline"); - baselineOption.IsRequired.ShouldBeTrue(); - baselineOption.Description.ShouldBe("Path to the folder or file with Baseline report."); - } - - [Fact] - public void CompareCommand_ShouldHave_TargetOption() - { - // Arrange & Act - var toolCommands = new ToolCommands(_provider); - - - // Assert - var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); - var targetOption = compareCommand.Options.Single(o => o.Name == "target"); - - targetOption.ValueType.ShouldBe(typeof(string)); - targetOption.Aliases.ShouldContain("-t"); - targetOption.Aliases.ShouldContain("--target"); - targetOption.IsRequired.ShouldBeTrue(); - targetOption.Description.ShouldBe("Path to the folder or file with target reports."); - } - - [Fact] - public void CompareCommand_ShouldHave_ThresholdMeanOption() - { - // Arrange & Act - var toolCommands = new ToolCommands(_provider); - - - // Assert - var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); - var meanThresholdOption = compareCommand.Options.Single(o => o.Name == "threshold-mean"); - - meanThresholdOption.ValueType.ShouldBe(typeof(string)); - meanThresholdOption.Aliases.ShouldContain("-tm"); - meanThresholdOption.Aliases.ShouldContain("--threshold-mean"); - meanThresholdOption.Description.ShouldBe("Throw an error when the mean threshold is met. Examples: 5%, 10ms, 10μs, 100ns, 1s."); - } - - [Fact] - public void CompareCommand_ShouldHave_ThresholdAllocationOption() - { - // Arrange & Act - var toolCommands = new ToolCommands(_provider); - - - // Assert - var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); - var allocationThresholdOption = compareCommand.Options.Single(o => o.Name == "threshold-allocation"); - - allocationThresholdOption.ValueType.ShouldBe(typeof(string)); - allocationThresholdOption.Aliases.ShouldContain("-ta"); - allocationThresholdOption.Aliases.ShouldContain("--threshold-allocation"); - allocationThresholdOption.Description.ShouldBe("Throw an error when the allocation threshold is met. Examples: 5%, 10b, 10kb, 100mb, 1gb."); - } - - [Fact] - public void CompareCommand_ShouldHave_FormatsOption() - { - // Arrange & Act - var toolCommands = new ToolCommands(_provider); - - - // Assert - var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); - var formatsOption = compareCommand.Options.Single(o => o.Name == "format"); - - formatsOption.ValueType.ShouldBe(typeof(string[])); - formatsOption.Aliases.ShouldContain("-f"); - formatsOption.Aliases.ShouldContain("--format"); - formatsOption.Description.ShouldBe("Output format for the report."); - } - - [Fact] - public void CompareCommand_ShouldHave_OutputOption() - { - // Arrange & Act - var toolCommands = new ToolCommands(_provider); - - - // Assert - var compareCommand = toolCommands.Subcommands.Single(c => c.Name == "compare"); - var outputOption = compareCommand.Options.Single(o => o.Name == "output"); - - outputOption.ValueType.ShouldBe(typeof(string)); - outputOption.Aliases.ShouldContain("-o"); - outputOption.Aliases.ShouldContain("--output"); - outputOption.Description.ShouldBe("Output directory to export the diff report. Default is current directory."); - } }