diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt index 66033c2..8238083 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt @@ -1,3 +1,12 @@ +Version: 1.2.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  +# Breaking Changes +- REMOVED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace and functionality has been merged into the BenchmarkProgram class to streamline the hosting and execution of benchmarks +  Version: 1.1.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt index ed4e30e..02fa13c 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 1.2.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version: 1.1.0 Availability: .NET 10 and .NET 9   diff --git a/CHANGELOG.md b/CHANGELOG.md index 732f847..dfd87e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. +## [1.2.0] - 2025-12-19 + +Technically, this is a major release due to the removal of BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace. However, since this library is new and the external API remains unchanged, we have decided to label this release as a minor update. + +### Removed + +- `BenchmarkWorker` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace has been removed. Its functionality has been merged into the `BenchmarkProgram` class to streamline the hosting and execution of benchmarks. + ## [1.1.0] - 2025-12-14 +This release introduces several enhancements and fixes to improve the functionality and usability of the `Codebelt.Extensions.BenchmarkDotNet` and `Codebelt.Extensions.BenchmarkDotNet.Console` packages. + ### Changed - `BenchmarkWorkspace` class in the Codebelt.Extensions.BenchmarkDotNet namespace was extended with two new static methods; `GetReportsResultsPath` and `GetReportsTuningPath` for retrieving the paths to the reports results and tuning directories respectively, diff --git a/Directory.Packages.props b/Directory.Packages.props index 3a92f84..0804ed5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,8 +5,8 @@ - - + + diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkWorkerBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkWorkerBenchmark-report-github.md deleted file mode 100644 index f02b08d..0000000 --- a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkWorkerBenchmark-report-github.md +++ /dev/null @@ -1,26 +0,0 @@ -``` - -BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) -12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores -.NET SDK 10.0.101 - [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 - Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 - Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 - -PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 -MinIterationCount=15 WarmupCount=1 - -``` -| Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | -|---------------------------------- |---------- |--------------:|------------:|------------:|--------------:|--------------:|--------------:|-------:|--------:|-------:|-------:|----------:|------------:| -| 'Construct BenchmarkWorker' | .NET 10.0 | 4.5280 ns | 0.2018 ns | 0.2324 ns | 4.5227 ns | 4.1448 ns | 5.0388 ns | 1.00 | 0.07 | 0.0020 | - | 32 B | 1.00 | -| 'Configure services' | .NET 10.0 | 67.0518 ns | 2.4825 ns | 2.6562 ns | 66.3017 ns | 64.0598 ns | 73.2110 ns | 14.84 | 0.93 | 0.0395 | - | 624 B | 19.50 | -| 'Configure services with options' | .NET 10.0 | 1,310.1326 ns | 35.2009 ns | 39.1257 ns | 1,292.2983 ns | 1,252.4459 ns | 1,383.2906 ns | 290.06 | 16.68 | 0.5072 | 0.0961 | 7976 B | 249.25 | -| 'Access configuration' | .NET 10.0 | 0.6325 ns | 0.0491 ns | 0.0435 ns | 0.6303 ns | 0.5750 ns | 0.7115 ns | 0.14 | 0.01 | - | - | - | 0.00 | -| 'Access environment' | .NET 10.0 | 0.6581 ns | 0.0604 ns | 0.0535 ns | 0.6514 ns | 0.5793 ns | 0.7540 ns | 0.15 | 0.01 | - | - | - | 0.00 | -| | | | | | | | | | | | | | | -| 'Construct BenchmarkWorker' | .NET 9.0 | 4.5442 ns | 0.1618 ns | 0.1799 ns | 4.4841 ns | 4.2818 ns | 4.8308 ns | 1.00 | 0.05 | 0.0020 | - | 32 B | 1.00 | -| 'Configure services' | .NET 9.0 | 78.0880 ns | 1.6229 ns | 1.8038 ns | 78.4799 ns | 75.0539 ns | 81.4879 ns | 17.21 | 0.76 | 0.0396 | - | 624 B | 19.50 | -| 'Configure services with options' | .NET 9.0 | 1,827.5207 ns | 215.5665 ns | 248.2466 ns | 1,916.0051 ns | 1,398.1489 ns | 2,121.1524 ns | 402.76 | 55.60 | 0.5046 | 0.1235 | 7944 B | 248.25 | -| 'Access configuration' | .NET 9.0 | 0.6918 ns | 0.0668 ns | 0.0714 ns | 0.6700 ns | 0.6141 ns | 0.8275 ns | 0.15 | 0.02 | - | - | - | 0.00 | -| 'Access environment' | .NET 9.0 | 0.6554 ns | 0.0526 ns | 0.0492 ns | 0.6580 ns | 0.5846 ns | 0.7320 ns | 0.14 | 0.01 | - | - | - | 0.00 | diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs index e71b672..91ea3c0 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs @@ -1,18 +1,26 @@ -using Codebelt.Bootstrapper.Console; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Filters; +using BenchmarkDotNet.Running; +using Codebelt.Bootstrapper.Console; using Cuemon; using Cuemon.Reflection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; namespace Codebelt.Extensions.BenchmarkDotNet.Console { /// /// Entry point helper for hosting and running benchmarks using BenchmarkDotNet. /// - /// - public class BenchmarkProgram : ConsoleProgram + /// + public class BenchmarkProgram : MinimalConsoleProgram { static BenchmarkProgram() { @@ -86,15 +94,101 @@ public static void Run(string[] args, Action public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace { - var hostBuilder = CreateHostBuilder(args); - hostBuilder.ConfigureServices(services => - { - services.AddSingleton(new BenchmarkContext(args)); - services.AddBenchmarkWorkspace(setup); - serviceConfigurator?.Invoke(services); - }); - using var host = hostBuilder.Build(); + var builder = CreateHostBuilder(args); + + builder.Services.Configure(o => o.SuppressStatusMessages = !IsDebugBuild); + builder.Services.AddSingleton(new BenchmarkContext(args)); + builder.Services.AddBenchmarkWorkspace(setup); + serviceConfigurator?.Invoke(builder.Services); + + using var host = builder.Build(); host.Run(); } + + /// + /// Runs the actual benchmarks as envisioned by BenchmarkDotNet. + /// + /// The service provider. + /// The cancellation token. + /// A completed task when benchmark execution has finished. + /// + /// When arguments are provided, they are forwarded to for selective execution. + /// After execution completes, artifact post-processing is performed. + /// + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + var options = serviceProvider.GetRequiredService(); + var workspace = serviceProvider.GetRequiredService(); + var assemblies = workspace.LoadBenchmarkAssemblies(); + var context = serviceProvider.GetRequiredService(); + + if (options.SkipBenchmarksWithReports) { ConfigureBenchmarkDotNetFiltersForExistingReports(options, assemblies); } + + try + { + ExecuteBenchmarks(assemblies, context, options); + } + finally + { + workspace.PostProcessArtifacts(); + } + + return Task.CompletedTask; + } + + private static void ConfigureBenchmarkDotNetFiltersForExistingReports(BenchmarkWorkspaceOptions options, Assembly[] assemblies) + { + var benchmarkTypes = assemblies + .SelectMany(a => a.GetTypes().Where(t => t.Name.EndsWith("Benchmark", StringComparison.Ordinal))) + .ToList(); + + options.ConfigureBenchmarkDotNet(c => ApplyReportFilters(c, options, benchmarkTypes)); + } + + private static IConfig ApplyReportFilters(IConfig config, BenchmarkWorkspaceOptions options, List benchmarkTypes) + { + var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); + if (!Directory.Exists(tuningPath)) { return config; } + + foreach (var report in Directory.EnumerateFiles(tuningPath)) + { + var matchingType = FindMatchingBenchmarkType(report, benchmarkTypes); + if (matchingType != null) + { + config = config.AddFilter(new SimpleFilter(bc => bc.Descriptor.Type != matchingType)); + } + } + + return config; + } + + private static Type FindMatchingBenchmarkType(string reportPath, List benchmarkTypes) + { + var filename = Path.GetFileNameWithoutExtension(reportPath); + var potentialTypeFullName = filename.Split('-').FirstOrDefault(); + if (string.IsNullOrWhiteSpace(potentialTypeFullName)) { return null; } + + var potentialTypeName = potentialTypeFullName.Split('.').LastOrDefault(); + if (string.IsNullOrWhiteSpace(potentialTypeName)) { return null; } + + return benchmarkTypes.FirstOrDefault(t => t.Name.Equals(potentialTypeName, StringComparison.OrdinalIgnoreCase)); + } + + private static void ExecuteBenchmarks(Assembly[] assemblies, BenchmarkContext context, BenchmarkWorkspaceOptions options) + { + if (context.Args.Length == 0) + { + foreach (var assembly in assemblies) + { + BenchmarkRunner.Run(assembly, options.Configuration); + } + } + else + { + BenchmarkSwitcher + .FromAssemblies(assemblies) + .Run(context.Args, options.Configuration); + } + } } } diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs deleted file mode 100644 index fb2ca5b..0000000 --- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs +++ /dev/null @@ -1,112 +0,0 @@ -using BenchmarkDotNet.Running; -using Codebelt.Bootstrapper.Console; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Filters; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console -{ - /// - /// Worker responsible for executing benchmarks within the console host. - /// - /// - public class BenchmarkWorker : ConsoleStartup - { - /// - /// Initializes a new instance of the class. - /// - /// The configuration. - /// The host environment. - public BenchmarkWorker(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) - { - } - - /// - /// Configures services for the benchmark runner. Override this method to customize service registration. - /// - /// The service collection to configure. - /// - /// Suppresses console lifetime status messages to keep benchmark output clean. - /// - public override void ConfigureServices(IServiceCollection services) - { - services.Configure(o => o.SuppressStatusMessages = !BenchmarkProgram.IsDebugBuild); - } - - /// - /// Runs the actual benchmarks as envisioned by BenchmarkDotNet. - /// - /// The service provider. - /// The cancellation token. - /// A completed task when benchmark execution has finished. - /// - /// When arguments are provided, they are forwarded to for selective execution. - /// After execution completes, the worker performs artifact post-processing. - /// - public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) - { - var options = serviceProvider.GetRequiredService(); - var workspace = serviceProvider.GetRequiredService(); - var assemblies = workspace.LoadBenchmarkAssemblies(); - var context = serviceProvider.GetRequiredService(); - - if (options.SkipBenchmarksWithReports) - { - var benchmarkTypes = assemblies.SelectMany(a => a.GetTypes().Where(t => t.Name.EndsWith("Benchmark", StringComparison.Ordinal))).ToList(); - options.ConfigureBenchmarkDotNet(c => - { - var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); - if (!Directory.Exists(tuningPath)) { return c; } - var reports = Directory.EnumerateFiles(tuningPath); - foreach (var report in reports) - { - var filename = Path.GetFileNameWithoutExtension(report); - var potentialTypeFullName = filename.Split('-').FirstOrDefault(); - if (string.IsNullOrWhiteSpace(potentialTypeFullName)) { continue; } - - var potentialTypeName = potentialTypeFullName.Split('.').LastOrDefault(); - if (string.IsNullOrWhiteSpace(potentialTypeName)) { continue; } - - var matchingType = benchmarkTypes.FirstOrDefault(t => t.Name.Equals(potentialTypeName, StringComparison.OrdinalIgnoreCase)); - - if (matchingType != null) - { - c = c.AddFilter(new SimpleFilter(bc => bc.Descriptor.Type != matchingType)); - } - } - return c; - }); - } - - try - { - if (context.Args.Length == 0) - { - foreach (var assembly in assemblies) - { - BenchmarkRunner.Run(assembly, options.Configuration); - } - } - else - { - BenchmarkSwitcher - .FromAssemblies(assemblies) - .Run(context.Args, options.Configuration); - } - } - finally - { - workspace.PostProcessArtifacts(); - } - - return Task.CompletedTask; - } - } -} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs index 58a2240..ca6932d 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs @@ -121,7 +121,7 @@ private static IEnumerable LoadAssemblies(string repositoryPath, strin var candidatePaths = Directory .EnumerateFiles(tuningDir, $"*.{benchmarkProjectSuffix}.dll", SearchOption.AllDirectories) - .Where(path => path.IndexOf(buildSegment, StringComparison.OrdinalIgnoreCase) >= 0); + .Where(path => path.Contains(buildSegment, StringComparison.OrdinalIgnoreCase)); foreach (var path in candidatePaths) { diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs new file mode 100644 index 0000000..69798b7 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Using abstractions in public APIs promotes encapsulation, testability, and long-term maintainability, as prescribed by the .NET Framework Design Guidelines.", Scope = "member", Target = "~M:Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspace.LoadAssemblies(System.String,System.String,System.String,System.String,System.Boolean)~System.Collections.Generic.IEnumerable{System.Reflection.Assembly}")] diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs index d89dca7..0740d64 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs @@ -248,7 +248,7 @@ public void BenchmarkProgram_ShouldInheritFromConsoleProgram() // Assert Assert.NotNull(baseType); Assert.True(baseType.IsGenericType); - Assert.Equal("ConsoleProgram`1", baseType.Name); + Assert.Equal("MinimalConsoleProgram`1", baseType.Name); TestOutput.WriteLine($"BenchmarkProgram correctly inherits from: {baseType.FullName}"); } @@ -263,7 +263,7 @@ public void BenchmarkProgram_ShouldUseCorrectGenericTypeParameter() // Assert Assert.NotNull(genericArguments); Assert.Single(genericArguments); - Assert.Equal(typeof(BenchmarkWorker), genericArguments[0]); + Assert.Equal(typeof(BenchmarkProgram), genericArguments[0]); TestOutput.WriteLine($"BenchmarkProgram uses correct generic type parameter: {genericArguments[0].Name}"); } diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs deleted file mode 100644 index ed4ec63..0000000 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs +++ /dev/null @@ -1,548 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using BenchmarkDotNet.Configs; -using Codebelt.Extensions.Xunit; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -/// -/// Tests for the class. -/// -public class BenchmarkWorkerTest : Test -{ - public BenchmarkWorkerTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void Constructor_ShouldInitialize_WithValidParameters() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - - // Act - var worker = new BenchmarkWorker(configuration, environment); - - // Assert - Assert.NotNull(worker); - } - - [Fact] - public void Constructor_ShouldAcceptNullConfiguration() - { - // Arrange - var environment = CreateMockHostEnvironment(); - - // Act - var worker = new BenchmarkWorker(null, environment); - - // Assert - Assert.NotNull(worker); - } - - [Fact] - public void Constructor_ShouldAcceptNullEnvironment() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - - // Act - var worker = new BenchmarkWorker(configuration, null); - - // Assert - Assert.NotNull(worker); - } - - [Fact] - public void Constructor_ShouldAcceptBothParametersNull() - { - // Act - var worker = new BenchmarkWorker(null, null); - - // Assert - Assert.NotNull(worker); - } - - [Fact] - public void BenchmarkWorker_ShouldInheritFromConsoleStartup() - { - // Act - var baseType = typeof(BenchmarkWorker).BaseType; - - // Assert - Assert.NotNull(baseType); - Assert.Equal("ConsoleStartup", baseType.Name); - - TestOutput.WriteLine($"BenchmarkWorker correctly inherits from: {baseType.FullName}"); - } - - [Fact] - public void BenchmarkWorker_ShouldBePublicClass() - { - // Act - var type = typeof(BenchmarkWorker); - - // Assert - Assert.True(type.IsPublic); - Assert.True(type.IsClass); - Assert.False(type.IsAbstract); - Assert.False(type.IsSealed); - - TestOutput.WriteLine("BenchmarkWorker is a public, non-abstract, non-sealed class"); - } - - [Fact] - public void ConfigureServices_ShouldExist_AndBePublic() - { - // Act - var method = typeof(BenchmarkWorker).GetMethod("ConfigureServices", BindingFlags.Public | BindingFlags.Instance); - - // Assert - Assert.NotNull(method); - Assert.True(method.IsPublic); - Assert.True(method.IsVirtual); - Assert.Equal(typeof(void), method.ReturnType); - - var parameters = method.GetParameters(); - Assert.Single(parameters); - Assert.Equal(typeof(IServiceCollection), parameters[0].ParameterType); - - TestOutput.WriteLine($"Found ConfigureServices method: {method}"); - } - - [Fact] - public void ConfigureServices_ShouldConfigureConsoleLifetimeOptions() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - var services = new ServiceCollection(); - - // Act - worker.ConfigureServices(services); - - // Assert - var serviceDescriptor = services.FirstOrDefault(s => - s.ServiceType.IsGenericType && - s.ServiceType.GetGenericTypeDefinition() == typeof(IConfigureOptions<>) && - s.ServiceType.GetGenericArguments()[0] == typeof(ConsoleLifetimeOptions)); - - Assert.NotNull(serviceDescriptor); - - TestOutput.WriteLine("ConsoleLifetimeOptions configuration was registered"); - } - - [Fact] - public void ConfigureServices_ShouldSuppressStatusMessages() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - var services = new ServiceCollection(); - - // Act - worker.ConfigureServices(services); - - // Build service provider and resolve options - var serviceProvider = services.BuildServiceProvider(); - var options = serviceProvider.GetService>()?.Value; - - // Assert - Assert.NotNull(options); - - if (BenchmarkProgram.IsDebugBuild) - { - Assert.False(options.SuppressStatusMessages); - } - else - { - Assert.True(options.SuppressStatusMessages); - } - - - TestOutput.WriteLine($"ConsoleLifetimeOptions.SuppressStatusMessages = {options.SuppressStatusMessages}"); - } - - [Fact] - public void ConfigureServices_ShouldHandleNullServiceCollection() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - // Act & Assert - Assert.Throws(() => worker.ConfigureServices(null)); - } - - [Fact] - public void RunAsync_ShouldExist_AndBePublic() - { - // Act - var method = typeof(BenchmarkWorker).GetMethod("RunAsync", BindingFlags.Public | BindingFlags.Instance); - - // Assert - Assert.NotNull(method); - Assert.True(method.IsPublic); - Assert.True(method.IsVirtual); - Assert.Equal(typeof(Task), method.ReturnType); - - var parameters = method.GetParameters(); - Assert.Equal(2, parameters.Length); - Assert.Equal(typeof(IServiceProvider), parameters[0].ParameterType); - Assert.Equal(typeof(CancellationToken), parameters[1].ParameterType); - - TestOutput.WriteLine($"Found RunAsync method: {method}"); - } - - [Fact] - public async Task RunAsync_ShouldCompleteSuccessfully_WithValidServiceProvider() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - var services = CreateServiceProviderWithMocks(); - var cancellationToken = CancellationToken.None; - - // Act - var task = worker.RunAsync(services, cancellationToken); - - // Assert - Assert.NotNull(task); - await task; - Assert.True(task.IsCompletedSuccessfully); - - TestOutput.WriteLine("RunAsync completed successfully"); - } - - [Fact] - public async Task RunAsync_ShouldReturnCompletedTask() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - var services = CreateServiceProviderWithMocks(); - var cancellationToken = CancellationToken.None; - - // Act - var task = worker.RunAsync(services, cancellationToken); - - // Assert - await task; - Assert.Equal(TaskStatus.RanToCompletion, task.Status); - Assert.False(task.IsFaulted); - Assert.False(task.IsCanceled); - - TestOutput.WriteLine($"Task status: {task.Status}"); - } - - [Fact] - public async Task RunAsync_ShouldHandleEmptyArgs() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - var services = CreateServiceProviderWithMocks(new string[] { }); - var cancellationToken = CancellationToken.None; - - // Act - var task = worker.RunAsync(services, cancellationToken); - - // Assert - await task; - Assert.True(task.IsCompletedSuccessfully); - - TestOutput.WriteLine("RunAsync handled empty args successfully"); - } - - [Fact] - public async Task RunAsync_ShouldHandleArgsWithValues() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - var args = new[] { "--filter", "MyBenchmark", "--job", "short" }; - var services = CreateServiceProviderWithMocks(args); - var cancellationToken = CancellationToken.None; - - // Act - var task = worker.RunAsync(services, cancellationToken); - - // Assert - await task; - Assert.True(task.IsCompletedSuccessfully); - - TestOutput.WriteLine($"RunAsync handled args with {args.Length} values successfully"); - } - - [Fact] - public async Task RunAsync_ShouldCallPostProcessArtifacts() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - var workspace = new FakeBenchmarkWorkspace(); - var services = CreateServiceProviderWithMocks(workspace: workspace); - var cancellationToken = CancellationToken.None; - - // Act - await worker.RunAsync(services, cancellationToken); - - // Assert - Assert.True(workspace.PostProcessArtifactsCalled); - - TestOutput.WriteLine("PostProcessArtifacts was called"); - } - - [Fact] - public async Task RunAsync_ShouldCallLoadBenchmarkAssemblies() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - var workspace = new FakeBenchmarkWorkspace(); - var services = CreateServiceProviderWithMocks(workspace: workspace); - var cancellationToken = CancellationToken.None; - - // Act - await worker.RunAsync(services, cancellationToken); - - // Assert - Assert.True(workspace.LoadBenchmarkAssembliesCalled); - - TestOutput.WriteLine("LoadBenchmarkAssemblies was called"); - } - - [Fact] - public async Task RunAsync_ShouldHandleCancellationToken() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - var services = CreateServiceProviderWithMocks(); - var cts = new CancellationTokenSource(); - - // Act - var task = worker.RunAsync(services, cts.Token); - - // Assert - await task; - Assert.True(task.IsCompletedSuccessfully); - - TestOutput.WriteLine("RunAsync handled cancellation token"); - } - - [Fact] - public async Task RunAsync_ShouldCallPostProcessArtifacts_EvenWithEmptyAssemblies() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - var workspace = new FakeBenchmarkWorkspace(Array.Empty()); - var services = CreateServiceProviderWithMocks(workspace: workspace); - var cancellationToken = CancellationToken.None; - - // Act - await worker.RunAsync(services, cancellationToken); - - // Assert - Assert.True(workspace.PostProcessArtifactsCalled); - - TestOutput.WriteLine("PostProcessArtifacts was called even with empty assemblies"); - } - - [Fact] - public void Constructor_ShouldHaveCorrectParameterNames() - { - // Act - var constructor = typeof(BenchmarkWorker).GetConstructors().Single(); - var parameters = constructor.GetParameters(); - - // Assert - Assert.Equal(2, parameters.Length); - Assert.Equal("configuration", parameters[0].Name); - Assert.Equal("environment", parameters[1].Name); - - TestOutput.WriteLine($"Constructor parameters: {string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"))}"); - } - - [Fact] - public void BenchmarkWorker_ShouldHaveXmlDocumentation() - { - // This test verifies that the class has XML documentation - // by checking for the summary element in the XML doc - - // Act - var type = typeof(BenchmarkWorker); - - // Assert - Assert.NotNull(type); - Assert.True(type.IsPublic); - - TestOutput.WriteLine($"BenchmarkWorker type: {type.FullName}"); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(3)] - [InlineData(5)] - public async Task RunAsync_ShouldHandleVariousArgCounts(int argCount) - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - var args = new string[argCount]; - for (int i = 0; i < argCount; i++) - { - args[i] = $"arg{i}"; - } - - var services = CreateServiceProviderWithMocks(args); - var cancellationToken = CancellationToken.None; - - // Act - var task = worker.RunAsync(services, cancellationToken); - - // Assert - await task; - Assert.True(task.IsCompletedSuccessfully); - - TestOutput.WriteLine($"RunAsync handled {argCount} arguments successfully"); - } - - [Fact] - public async Task RunAsync_ShouldUseConfiguration_FromOptions() - { - // Arrange - var configuration = new ConfigurationBuilder().Build(); - var environment = CreateMockHostEnvironment(); - var worker = new BenchmarkWorker(configuration, environment); - - var customConfig = ManualConfig.CreateEmpty(); - var options = new BenchmarkWorkspaceOptions { Configuration = customConfig }; - var services = CreateServiceProviderWithMocks(options: options); - var cancellationToken = CancellationToken.None; - - // Act - await worker.RunAsync(services, cancellationToken); - - // Assert - Assert.True(true); // If we get here without exception, the configuration was used - - TestOutput.WriteLine("RunAsync used configuration from options"); - } - - [Fact] - public void ConfigureServices_ShouldBeOverridable() - { - // Arrange - var method = typeof(BenchmarkWorker).GetMethod("ConfigureServices"); - - // Assert - Assert.NotNull(method); - Assert.True(method.IsVirtual); - Assert.False(method.IsFinal); - - TestOutput.WriteLine("ConfigureServices is virtual and can be overridden"); - } - - [Fact] - public void RunAsync_ShouldBeOverridable() - { - // Arrange - var method = typeof(BenchmarkWorker).GetMethod("RunAsync"); - - // Assert - Assert.NotNull(method); - Assert.True(method.IsVirtual); - Assert.False(method.IsFinal); - - TestOutput.WriteLine("RunAsync is virtual and can be overridden"); - } - - private static IHostEnvironment CreateMockHostEnvironment() - { - var environment = new FakeHostEnvironment - { - EnvironmentName = "Test", - ApplicationName = "BenchmarkWorkerTest", - ContentRootPath = AppContext.BaseDirectory - }; - return environment; - } - - private static IServiceProvider CreateServiceProviderWithMocks(string[] args = null, BenchmarkWorkspaceOptions options = null, FakeBenchmarkWorkspace workspace = null) - { - var services = new ServiceCollection(); - - // Add required services - services.AddSingleton(options ?? new BenchmarkWorkspaceOptions()); - services.AddSingleton(workspace ?? new FakeBenchmarkWorkspace()); - services.AddSingleton(new BenchmarkContext(args ?? Array.Empty())); - - return services.BuildServiceProvider(); - } - - private class FakeHostEnvironment : IHostEnvironment - { - public string EnvironmentName { get; set; } - public string ApplicationName { get; set; } - public string ContentRootPath { get; set; } - public IFileProvider ContentRootFileProvider { get; set; } - } - - private class FakeBenchmarkWorkspace : IBenchmarkWorkspace - { - private readonly Assembly[] _assemblies; - - public FakeBenchmarkWorkspace(Assembly[] assemblies = null) - { - _assemblies = assemblies ?? new[] { typeof(BenchmarkWorkerTest).Assembly }; - } - - public bool LoadBenchmarkAssembliesCalled { get; private set; } - public bool PostProcessArtifactsCalled { get; private set; } - - public Assembly[] LoadBenchmarkAssemblies() - { - LoadBenchmarkAssembliesCalled = true; - return _assemblies; - } - - public void PostProcessArtifacts() - { - PostProcessArtifactsCalled = true; - } - } -} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkWorkerBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkWorkerBenchmark.cs deleted file mode 100644 index 6c0e214..0000000 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkWorkerBenchmark.cs +++ /dev/null @@ -1,92 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; -using System.Collections.Generic; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -/// -/// Benchmarks for the class. -/// -[MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -public class BenchmarkWorkerBenchmark -{ - private IConfiguration _configuration; - private IHostEnvironment _environment; - private BenchmarkWorker _worker; - private IServiceCollection _services; - - [GlobalSetup] - public void Setup() - { - // Deterministic initialization of test data - var configData = new Dictionary - { - ["Setting1"] = "Value1", - ["Setting2"] = "Value2" - }; - - _configuration = new ConfigurationBuilder() - .AddInMemoryCollection(configData) - .Build(); - - _environment = new TestHostEnvironment - { - EnvironmentName = "Development", - ApplicationName = "BenchmarkTest" - }; - - _worker = new BenchmarkWorker(_configuration, _environment); - _services = new ServiceCollection(); - } - - [Benchmark(Baseline = true, Description = "Construct BenchmarkWorker")] - public BenchmarkWorker ConstructWorker() - { - return new BenchmarkWorker(_configuration, _environment); - } - - [Benchmark(Description = "Configure services")] - public IServiceCollection ConfigureServices() - { - var services = new ServiceCollection(); - _worker.ConfigureServices(services); - return services; - } - - [Benchmark(Description = "Configure services with options")] - public IServiceCollection ConfigureServicesWithOptions() - { - var services = new ServiceCollection(); - _worker.ConfigureServices(services); - var provider = services.BuildServiceProvider(); - return services; - } - - [Benchmark(Description = "Access configuration")] - public IConfiguration AccessConfiguration() - { - return _configuration; - } - - [Benchmark(Description = "Access environment")] - public IHostEnvironment AccessEnvironment() - { - return _environment; - } - - /// - /// Test implementation of IHostEnvironment for benchmark purposes. - /// - private class TestHostEnvironment : IHostEnvironment - { - public string EnvironmentName { get; set; } - public string ApplicationName { get; set; } - public string ContentRootPath { get; set; } - public IFileProvider ContentRootFileProvider { get; set; } - } -}