From 392b0826df930f1ad5a7bac713c6e012f0683ab1 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 22 Oct 2026 18:01:31 +0200 Subject: [PATCH 01/20] =?UTF-8?q?=F0=9F=8E=A8=20convert=20touched=20source?= =?UTF-8?q?s=20to=20file-scoped=20namespaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the touched C# files with the file-scoped namespace rule enforced by .editorconfig. No behavior changes; the namespaces and type declarations are unchanged. --- .../BenchmarkProgram.cs | 489 +++++++++--------- .../BenchmarkWorkspaceOptionsExtensions.cs | 155 +++--- .../ServiceCollectionExtensions.cs | 89 ++-- .../ServiceCollectionExtensionsTest.cs | 153 +++--- .../Program.cs | 49 +- 5 files changed, 465 insertions(+), 470 deletions(-) diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs index f10d006..17d18f2 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs @@ -1,260 +1,259 @@ -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 : MinimalConsoleProgram - { - static BenchmarkProgram() - { - var isDebugBuild = Decorator.Enclose(Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).IsDebugBuild(); - BuildConfiguration = isDebugBuild ? "Debug" : "Release"; - IsDebugBuild = isDebugBuild; - } - - /// - /// Gets the build configuration of the entry assembly. - /// - /// The value is either Debug or Release. - public static string BuildConfiguration { get; } - - /// - /// Gets a value indicating whether the entry assembly was built in Debug configuration. - /// - /// true if the entry assembly was compiled with debugging information; otherwise, false. - public static bool IsDebugBuild { get; } - - /// - /// Runs benchmarks using the default implementation. - /// - /// The command-line arguments passed to the application. - /// The which may be configured. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. - /// - public static void Run(string[] args, Action setup = null) - { - Run(args, null, setup); - } - - /// - /// Runs benchmarks using the default implementation. - /// - /// The command-line arguments passed to the application. - /// The delegate that will be invoked to configure additional services in the . - /// The which may be configured. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. - /// - public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null) - { - Run(args, serviceConfigurator, setup); - } - - /// - /// Runs benchmarks using a custom implementation of . - /// - /// The type of the workspace that implements . - /// The command-line arguments passed to the application. - /// The which may be configured. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. - /// - public static void Run(string[] args, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace - { - Run(args, null, setup); - } - - /// - /// Runs benchmarks using a custom implementation of . - /// - /// The type of the workspace that implements . - /// The command-line arguments passed to the application. - /// The delegate that will be invoked to configure additional services in the . - /// The which may be configured. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. - /// - public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace - { - using var host = BuildHost(args, serviceConfigurator, setup); - host.Run(); - } +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; - /// - /// Runs benchmarks asynchronously using the default implementation. - /// - /// The command-line arguments passed to the application. - /// The which may be configured. - /// A task that represents the asynchronous benchmark host operation. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. - /// - public static Task RunAsync(string[] args, Action setup = null) - { - return RunAsync(args, null, setup); - } +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +/// +/// Entry point helper for hosting and running benchmarks using BenchmarkDotNet. +/// +/// +public class BenchmarkProgram : MinimalConsoleProgram +{ + static BenchmarkProgram() + { + var isDebugBuild = Decorator.Enclose(Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).IsDebugBuild(); + BuildConfiguration = isDebugBuild ? "Debug" : "Release"; + IsDebugBuild = isDebugBuild; + } + + /// + /// Gets the build configuration of the entry assembly. + /// + /// The value is either Debug or Release. + public static string BuildConfiguration { get; } + + /// + /// Gets a value indicating whether the entry assembly was built in Debug configuration. + /// + /// true if the entry assembly was compiled with debugging information; otherwise, false. + public static bool IsDebugBuild { get; } + + /// + /// Runs benchmarks using the default implementation. + /// + /// The command-line arguments passed to the application. + /// The which may be configured. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. + /// + public static void Run(string[] args, Action setup = null) + { + Run(args, null, setup); + } + + /// + /// Runs benchmarks using the default implementation. + /// + /// The command-line arguments passed to the application. + /// The delegate that will be invoked to configure additional services in the . + /// The which may be configured. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. + /// + public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null) + { + Run(args, serviceConfigurator, setup); + } + + /// + /// Runs benchmarks using a custom implementation of . + /// + /// The type of the workspace that implements . + /// The command-line arguments passed to the application. + /// The which may be configured. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. + /// + public static void Run(string[] args, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + Run(args, null, setup); + } + + /// + /// Runs benchmarks using a custom implementation of . + /// + /// The type of the workspace that implements . + /// The command-line arguments passed to the application. + /// The delegate that will be invoked to configure additional services in the . + /// The which may be configured. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. + /// + public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + using var host = BuildHost(args, serviceConfigurator, setup); + host.Run(); + } + + /// + /// Runs benchmarks asynchronously using the default implementation. + /// + /// The command-line arguments passed to the application. + /// The which may be configured. + /// A task that represents the asynchronous benchmark host operation. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. + /// + public static Task RunAsync(string[] args, Action setup = null) + { + return RunAsync(args, null, setup); + } + + /// + /// Runs benchmarks asynchronously using the default implementation. + /// + /// The command-line arguments passed to the application. + /// The delegate that will be invoked to configure additional services in the . + /// The which may be configured. + /// A task that represents the asynchronous benchmark host operation. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. + /// + public static Task RunAsync(string[] args, Action serviceConfigurator = null, Action setup = null) + { + return RunAsync(args, serviceConfigurator, setup); + } + + /// + /// Runs benchmarks asynchronously using a custom implementation of . + /// + /// The type of the workspace that implements . + /// The command-line arguments passed to the application. + /// The which may be configured. + /// A task that represents the asynchronous benchmark host operation. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. + /// + public static Task RunAsync(string[] args, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + return RunAsync(args, null, setup); + } + + /// + /// Runs benchmarks asynchronously using a custom implementation of . + /// + /// The type of the workspace that implements . + /// The command-line arguments passed to the application. + /// The delegate that will be invoked to configure additional services in the . + /// The which may be configured. + /// A task that represents the asynchronous benchmark host operation. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. + /// + public static async Task RunAsync(string[] args, Action serviceConfigurator = null, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + using var host = BuildHost(args, serviceConfigurator, setup); + await host.RunAsync().ConfigureAwait(false); + } + + /// + /// 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(); - /// - /// Runs benchmarks asynchronously using the default implementation. - /// - /// The command-line arguments passed to the application. - /// The delegate that will be invoked to configure additional services in the . - /// The which may be configured. - /// A task that represents the asynchronous benchmark host operation. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. - /// - public static Task RunAsync(string[] args, Action serviceConfigurator = null, Action setup = null) + if (options.SkipBenchmarksWithReports) { ConfigureBenchmarkDotNetFiltersForExistingReports(options, assemblies); } + + try { - return RunAsync(args, serviceConfigurator, setup); + ExecuteBenchmarks(assemblies, context, options); } - - /// - /// Runs benchmarks asynchronously using a custom implementation of . - /// - /// The type of the workspace that implements . - /// The command-line arguments passed to the application. - /// The which may be configured. - /// A task that represents the asynchronous benchmark host operation. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. - /// - public static Task RunAsync(string[] args, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + finally { - return RunAsync(args, null, setup); + workspace.PostProcessArtifacts(); } - /// - /// Runs benchmarks asynchronously using a custom implementation of . - /// - /// The type of the workspace that implements . - /// The command-line arguments passed to the application. - /// The delegate that will be invoked to configure additional services in the . - /// The which may be configured. - /// A task that represents the asynchronous benchmark host operation. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. - /// - public static async Task RunAsync(string[] args, Action serviceConfigurator = null, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + return Task.CompletedTask; + } + + private static IHost BuildHost(string[] args, Action serviceConfigurator, Action setup) where TWorkspace : class, IBenchmarkWorkspace + { + 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); + + return builder.Build(); + } + + 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)) { - using var host = BuildHost(args, serviceConfigurator, setup); - await host.RunAsync().ConfigureAwait(false); + var matchingType = FindMatchingBenchmarkType(report, benchmarkTypes); + if (matchingType != null) + { + config = config.AddFilter(new SimpleFilter(bc => bc.Descriptor.Type != matchingType)); + } } - /// - /// 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; - } + return config; + } - private static IHost BuildHost(string[] args, Action serviceConfigurator, Action setup) where TWorkspace : class, IBenchmarkWorkspace - { - var builder = CreateHostBuilder(args); + private static Type FindMatchingBenchmarkType(string reportPath, List benchmarkTypes) + { + var filename = Path.GetFileNameWithoutExtension(reportPath); + var potentialTypeFullName = filename.Split('-').FirstOrDefault(); + if (string.IsNullOrWhiteSpace(potentialTypeFullName)) { return null; } - builder.Services.Configure(o => o.SuppressStatusMessages = !IsDebugBuild); - builder.Services.AddSingleton(new BenchmarkContext(args)); - builder.Services.AddBenchmarkWorkspace(setup); - serviceConfigurator?.Invoke(builder.Services); + var potentialTypeName = potentialTypeFullName.Split('.').LastOrDefault(); + if (string.IsNullOrWhiteSpace(potentialTypeName)) { return null; } - return builder.Build(); - } + return benchmarkTypes.FirstOrDefault(t => t.Name.Equals(potentialTypeName, StringComparison.OrdinalIgnoreCase)); + } - private static void ConfigureBenchmarkDotNetFiltersForExistingReports(BenchmarkWorkspaceOptions options, Assembly[] assemblies) + private static void ExecuteBenchmarks(Assembly[] assemblies, BenchmarkContext context, BenchmarkWorkspaceOptions options) + { + if (context.Args.Length == 0) { - 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); - } - } - } -} + 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/BenchmarkWorkspaceOptionsExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs index a16659a..d4e3446 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs @@ -1,78 +1,77 @@ -using System; -using BenchmarkDotNet.Configs; -using Cuemon; - -namespace Codebelt.Extensions.BenchmarkDotNet -{ - /// - /// Extension methods for the class. - /// - public static class BenchmarkWorkspaceOptionsExtensions - { - /// - /// Configures the BenchmarkDotNet configuration for the specified . - /// - /// The to extend. - /// The function delegate that configures the . - /// The instance for chaining. - /// - /// cannot be null -or- - /// cannot be null. - /// - /// - /// must not return null. - /// - /// - /// - /// BenchmarkDotNet's configuration model is intentionally immutable-ish: methods such as - /// AddJob, AddColumn, and AddDiagnoser do not mutate the - /// incoming instance. Instead, they produce and return a new - /// configuration object. This behavior is powerful but can be unintuitive when used inside - /// option delegates where callers naturally expect fluent configuration to modify the - /// underlying options instance. - /// - /// - /// Without this helper, callers would need to explicitly reassign: - /// - /// - /// options.Configuration = options.Configuration.AddJob(job); - /// - /// - /// This extension method abstracts away that requirement by: - /// - /// - /// - /// Forcing initialization of the default configuration if needed, - /// - /// - /// Passing the current configuration to the delegate for fluent BenchmarkDotNet operations, - /// - /// - /// Assigning the delegate’s returned configuration back to . - /// - /// - /// - /// This preserves BenchmarkDotNet's design while providing an intuitive experience for users configuring via delegates. - /// - /// - /// - /// cannot be null -or- - /// cannot be null. - /// - /// - /// must not return null. - /// - public static BenchmarkWorkspaceOptions ConfigureBenchmarkDotNet(this BenchmarkWorkspaceOptions options, Func configure) - { - Validator.ThrowIfNull(options); - Validator.ThrowIfNull(configure); - - var current = options.Configuration; // forces default config if needed - var updated = configure(current) ?? throw new InvalidOperationException("Configuration delegate must not return null."); - - options.Configuration = updated; - - return options; - } - } -} +using System; +using BenchmarkDotNet.Configs; +using Cuemon; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Extension methods for the class. +/// +public static class BenchmarkWorkspaceOptionsExtensions +{ + /// + /// Configures the BenchmarkDotNet configuration for the specified . + /// + /// The to extend. + /// The function delegate that configures the . + /// The instance for chaining. + /// + /// cannot be null -or- + /// cannot be null. + /// + /// + /// must not return null. + /// + /// + /// + /// BenchmarkDotNet's configuration model is intentionally immutable-ish: methods such as + /// AddJob, AddColumn, and AddDiagnoser do not mutate the + /// incoming instance. Instead, they produce and return a new + /// configuration object. This behavior is powerful but can be unintuitive when used inside + /// option delegates where callers naturally expect fluent configuration to modify the + /// underlying options instance. + /// + /// + /// Without this helper, callers would need to explicitly reassign: + /// + /// + /// options.Configuration = options.Configuration.AddJob(job); + /// + /// + /// This extension method abstracts away that requirement by: + /// + /// + /// + /// Forcing initialization of the default configuration if needed, + /// + /// + /// Passing the current configuration to the delegate for fluent BenchmarkDotNet operations, + /// + /// + /// Assigning the delegate’s returned configuration back to . + /// + /// + /// + /// This preserves BenchmarkDotNet's design while providing an intuitive experience for users configuring via delegates. + /// + /// + /// + /// cannot be null -or- + /// cannot be null. + /// + /// + /// must not return null. + /// + public static BenchmarkWorkspaceOptions ConfigureBenchmarkDotNet(this BenchmarkWorkspaceOptions options, Func configure) + { + Validator.ThrowIfNull(options); + Validator.ThrowIfNull(configure); + + var current = options.Configuration; // forces default config if needed + var updated = configure(current) ?? throw new InvalidOperationException("Configuration delegate must not return null."); + + options.Configuration = updated; + + return options; + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs index 8030e1f..6035161 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs @@ -1,45 +1,44 @@ -using System; -using Cuemon; -using Microsoft.Extensions.DependencyInjection; - -namespace Codebelt.Extensions.BenchmarkDotNet -{ - /// - /// Extension methods for the interface. - /// - public static class ServiceCollectionExtensions - { - /// - /// Adds the default benchmark workspace implementation () to the specified - /// - /// The to add the services to. - /// The which may be configured. - /// The original instance for chaining. - public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection services, Action setup = null) - { - return AddBenchmarkWorkspace(services, setup); - } - - /// - /// Adds a benchmark workspace implementation of type to the specified - /// - /// The type that implements the interface. - /// The to add the services to. - /// The which may be configured. - /// The original instance for chaining. - /// - /// Validates the parameter and the provided configurator. - /// Registers as a singleton using , - /// applies the provided configuration, and registers the resolved as a singleton. - /// - public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection services, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace - { - Validator.ThrowIfNull(services); - Validator.ThrowIfInvalidConfigurator(setup, out var options); - return services - .AddSingleton() - .Configure(setup ?? (_ => {})) - .AddSingleton(options); - } - } -} +using System; +using Cuemon; +using Microsoft.Extensions.DependencyInjection; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Extension methods for the interface. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds the default benchmark workspace implementation () to the specified + /// + /// The to add the services to. + /// The which may be configured. + /// The original instance for chaining. + public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection services, Action setup = null) + { + return AddBenchmarkWorkspace(services, setup); + } + + /// + /// Adds a benchmark workspace implementation of type to the specified + /// + /// The type that implements the interface. + /// The to add the services to. + /// The which may be configured. + /// The original instance for chaining. + /// + /// Validates the parameter and the provided configurator. + /// Registers as a singleton using , + /// applies the provided configuration, and registers the resolved as a singleton. + /// + public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection services, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + Validator.ThrowIfNull(services); + Validator.ThrowIfInvalidConfigurator(setup, out var options); + return services + .AddSingleton() + .Configure(setup ?? (_ => {})) + .AddSingleton(options); + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs index 5f490e4..697c70c 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs @@ -1,77 +1,76 @@ -using System; -using System.Reflection; -using Codebelt.Extensions.Xunit; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet -{ - public class ServiceCollectionExtensionsTest : Test - { - public ServiceCollectionExtensionsTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void AddBenchmarkWorkspace_ShouldThrowWhenServicesIsNull() - { - Assert.Throws(() => ServiceCollectionExtensions.AddBenchmarkWorkspace((IServiceCollection)null)); - } - - [Fact] - public void AddBenchmarkWorkspace_ShouldRegisterDefaultWorkspaceAndOptions_WhenCalledWithoutGeneric() - { - var services = new ServiceCollection(); - services.AddBenchmarkWorkspace(setup: options => options.BenchmarkProjectSuffix = "MySuffix"); - using var sp = services.BuildServiceProvider(); - - var workspace = sp.GetRequiredService(); - Assert.IsType(workspace); - - var options = sp.GetRequiredService(); - Assert.Equal("MySuffix", options.BenchmarkProjectSuffix); - } - - [Fact] - public void AddBenchmarkWorkspace_GenericOverload_ShouldRegisterCustomImplementationAndOptions() - { - var services = new ServiceCollection(); - services.AddBenchmarkWorkspace(setup: options => options.RepositoryPath = "repo-path"); - using var sp = services.BuildServiceProvider(); - - var workspace = sp.GetRequiredService(); - Assert.IsType(workspace); - - var options = sp.GetRequiredService(); - Assert.Equal("repo-path", options.RepositoryPath); - } - - [Fact] - public void AddBenchmarkWorkspace_GenericOverload_ShouldUseNullCoalescingLambda_WhenSetupIsNull() - { - // When setup is null, the Configure call registers a no-op lambda (_ => {}). - // Resolving IOptions.Value causes the Options framework to - // invoke all registered configure-actions, which executes the null-coalescing lambda body. - var services = new ServiceCollection(); - services.AddBenchmarkWorkspace(setup: null); - using var sp = services.BuildServiceProvider(); - - // Resolving via IOptions triggers the registered configure action (the no-op lambda) - var optionsAccessor = sp.GetRequiredService>(); - var options = optionsAccessor.Value; - - Assert.NotNull(options); - Assert.IsType(sp.GetRequiredService()); - - TestOutput.WriteLine($"BenchmarkProjectSuffix: {options.BenchmarkProjectSuffix}"); - } - - private sealed class FakeWorkspace : IBenchmarkWorkspace - { - public Assembly[] LoadBenchmarkAssemblies() => Array.Empty(); - - public void PostProcessArtifacts() { } - } - } -} +using System; +using System.Reflection; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +public class ServiceCollectionExtensionsTest : Test +{ + public ServiceCollectionExtensionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void AddBenchmarkWorkspace_ShouldThrowWhenServicesIsNull() + { + Assert.Throws(() => ServiceCollectionExtensions.AddBenchmarkWorkspace((IServiceCollection)null)); + } + + [Fact] + public void AddBenchmarkWorkspace_ShouldRegisterDefaultWorkspaceAndOptions_WhenCalledWithoutGeneric() + { + var services = new ServiceCollection(); + services.AddBenchmarkWorkspace(setup: options => options.BenchmarkProjectSuffix = "MySuffix"); + using var sp = services.BuildServiceProvider(); + + var workspace = sp.GetRequiredService(); + Assert.IsType(workspace); + + var options = sp.GetRequiredService(); + Assert.Equal("MySuffix", options.BenchmarkProjectSuffix); + } + + [Fact] + public void AddBenchmarkWorkspace_GenericOverload_ShouldRegisterCustomImplementationAndOptions() + { + var services = new ServiceCollection(); + services.AddBenchmarkWorkspace(setup: options => options.RepositoryPath = "repo-path"); + using var sp = services.BuildServiceProvider(); + + var workspace = sp.GetRequiredService(); + Assert.IsType(workspace); + + var options = sp.GetRequiredService(); + Assert.Equal("repo-path", options.RepositoryPath); + } + + [Fact] + public void AddBenchmarkWorkspace_GenericOverload_ShouldUseNullCoalescingLambda_WhenSetupIsNull() + { + // When setup is null, the Configure call registers a no-op lambda (_ => {}). + // Resolving IOptions.Value causes the Options framework to + // invoke all registered configure-actions, which executes the null-coalescing lambda body. + var services = new ServiceCollection(); + services.AddBenchmarkWorkspace(setup: null); + using var sp = services.BuildServiceProvider(); + + // Resolving via IOptions triggers the registered configure action (the no-op lambda) + var optionsAccessor = sp.GetRequiredService>(); + var options = optionsAccessor.Value; + + Assert.NotNull(options); + Assert.IsType(sp.GetRequiredService()); + + TestOutput.WriteLine($"BenchmarkProjectSuffix: {options.BenchmarkProjectSuffix}"); + } + + private sealed class FakeWorkspace : IBenchmarkWorkspace + { + public Assembly[] LoadBenchmarkAssemblies() => Array.Empty(); + + public void PostProcessArtifacts() { } + } +} diff --git a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs index d639e65..ac736a1 100644 --- a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs +++ b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs @@ -1,25 +1,24 @@ -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Jobs; -using Codebelt.Extensions.BenchmarkDotNet.Console; - -namespace Codebelt.Extensions.BenchmarkDotNet.Runner -{ - public class Program - { - public static void Main(string[] args) - { - BenchmarkProgram.Run(args, o => - { - o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; - o.SkipBenchmarksWithReports = true; - o.ConfigureBenchmarkDotNet(c => - { - var slimJob = BenchmarkWorkspaceOptions.Slim; - return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) - .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); - }); - }); - } - } -} +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using Codebelt.Extensions.BenchmarkDotNet.Console; + +namespace Codebelt.Extensions.BenchmarkDotNet.Runner; + +public class Program +{ + public static void Main(string[] args) + { + BenchmarkProgram.Run(args, o => + { + o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; + o.SkipBenchmarksWithReports = true; + o.ConfigureBenchmarkDotNet(c => + { + var slimJob = BenchmarkWorkspaceOptions.Slim; + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) + .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + }); + }); + } +} From ec4b47e36a3e3290abb6a2329efd85dbffbd95ea Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 22 Oct 2026 18:01:35 +0200 Subject: [PATCH 02/20] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20bump=20coverlet=20an?= =?UTF-8?q?d=20bootstrapper=20package=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the central package versions to the latest stable releases of Codebelt.Coverlet.MTP and Codebelt.Bootstrapper.Console so the build and tooling stay current. --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9d589af..43fa915 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,10 +3,10 @@ true - + - + @@ -16,4 +16,4 @@ - + \ No newline at end of file From 6c9b7455b954659b6527d6c92800323e268e46b5 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 22 Oct 2026 18:01:36 +0200 Subject: [PATCH 03/20] =?UTF-8?q?=F0=9F=94=A7=20build=20net11.0=20and=20ne?= =?UTF-8?q?t10.0=20target=20frameworks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the source, test, and benchmark projects from net9.0 to net11.0 alongside net10.0. The functional tests now always reference the runner project, since the net10.0-only condition no longer selects a single framework. --- Directory.Build.props | 6 +++--- ...tensions.BenchmarkDotNet.FunctionalTests.csproj | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0283897..ea668f7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ - net10.0;net9.0 + net11.0;net10.0 Copyright © Geekle 2025-2026. All rights reserved. gimlichael Geekle @@ -56,7 +56,7 @@ - net10.0;net9.0 + net11.0;net10.0 false Exe false @@ -94,7 +94,7 @@ - net10.0;net9.0 + net11.0;net10.0 false false false diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests.csproj b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests.csproj index c1c3f64..d7bcb28 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests.csproj +++ b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests.csproj @@ -1,13 +1,13 @@ - - - - Codebelt.Extensions.BenchmarkDotNet - - + + + + Codebelt.Extensions.BenchmarkDotNet + + - + From e6f072ba4f37ed73ec79ebd6649667b403240923 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 22 Oct 2026 18:01:36 +0200 Subject: [PATCH 04/20] =?UTF-8?q?=F0=9F=91=B7=20resolve=20service=20update?= =?UTF-8?q?=20version=20from=20git=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The service update workflow now derives the next version from the latest stable vMAJOR.MINOR.PATCH tag and validates it against CHANGELOG.md, instead of trusting the changelog heading alone. The changelog entry is reused when a pending TBD release already exists. --- .github/workflows/service-update.yml | 93 ++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/.github/workflows/service-update.yml b/.github/workflows/service-update.yml index f9a235e..1f69586 100644 --- a/.github/workflows/service-update.yml +++ b/.github/workflows/service-update.yml @@ -31,6 +31,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + fetch-tags: true - name: Resolve trigger inputs id: trigger @@ -42,13 +43,68 @@ jobs: - name: Determine new version for this repo id: newver + shell: bash run: | - CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1) - NEW=$(echo "$CURRENT" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}') - BRANCH="v${NEW}/service-update" - echo "current=$CURRENT" >> $GITHUB_OUTPUT - echo "new=$NEW" >> $GITHUB_OUTPUT - echo "branch=$BRANCH" >> $GITHUB_OUTPUT + python3 - <<'PY' + import os + import re + import subprocess + import sys + + tag_pattern = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") + tags = subprocess.check_output(["git", "tag", "--list"], text=True).splitlines() + stable_tags = [] + for tag in tags: + match = tag_pattern.fullmatch(tag) + if match: + version = tuple(int(part) for part in match.groups()) + stable_tags.append((version, tag)) + + if not stable_tags: + print("::error::No stable vMAJOR.MINOR.PATCH Git tag was found.") + sys.exit(1) + + _, latest_tag = max(stable_tags, key=lambda item: item[0]) + latest_version = latest_tag[1:] + major, minor, patch = (int(part) for part in latest_version.split(".")) + next_version = f"{major}.{minor}.{patch + 1}" + + with open("CHANGELOG.md", encoding="utf-8") as changelog_file: + changelog = changelog_file.read() + + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - ([^\r\n]+)[ \t]*$", changelog) + if not heading: + print("::error::CHANGELOG.md has no versioned heading to compare with Git tags.") + sys.exit(1) + + current_version, current_status = heading.groups() + current_status = current_status.strip() + if current_status.upper() != "TBD" and not re.fullmatch(r"\d{4}-\d{2}-\d{2}", current_status): + print(f"::error::Unsupported CHANGELOG.md version status: {current_status}") + sys.exit(1) + + if current_version == next_version and current_status.upper() == "TBD": + # Reuse an already pending next release, such as [5.0.13] - TBD. + new_version = next_version + elif current_version == latest_version and current_status.upper() != "TBD": + # The changelog is at the latest released tag; start the next patch release. + new_version = next_version + else: + print( + f"::error::CHANGELOG.md starts at {current_version} ({current_status}), " + f"but the latest stable Git tag is {latest_tag}. Expected either " + f"[{latest_version}] with a release date or [{next_version}] - TBD." + ) + sys.exit(1) + + branch = f"v{new_version}/service-update" + print(f"Latest stable Git tag: {latest_tag}") + print(f"Latest CHANGELOG.md version: {current_version} ({current_status})") + print(f"Service update version: {new_version}") + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"current={current_version}\n") + output.write(f"new={new_version}\n") + output.write(f"branch={branch}\n") - name: Generate codebelt-aicia token id: app-token @@ -81,14 +137,27 @@ jobs: from datetime import date new_ver = os.environ['NEW_VERSION'] today = date.today().isoformat() - entry = f"## [{new_ver}] - {today}\n\nThis is a service update that focuses on package dependencies.\n\n" - with open("CHANGELOG.md") as f: + service_note = "This release also includes a service update focused on package dependencies." + with open("CHANGELOG.md", encoding="utf-8") as f: content = f.read() - idx = content.find("## [") - content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry) - with open("CHANGELOG.md", "w") as f: + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - (TBD|\d{4}-\d{2}-\d{2})[ \t]*$", content) + if heading and heading.group(1) == new_ver: + if heading.group(2) == "TBD": + content = content[:heading.start(2)] + today + content[heading.end(2):] + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - \d{4}-\d{2}-\d{2}[ \t]*$", content) + section_end = re.search(r"(?m)^## \[", content[heading.end():]) + end = heading.end() + section_end.start() if section_end else len(content) + if service_note not in content[heading.end():end]: + remainder = content[heading.end():] + content = content[:heading.end()] + "\n\n" + service_note + "\n\n" + remainder.lstrip("\r\n") + print(f"Updated existing CHANGELOG entry for v{new_ver}") + else: + entry = f"## [{new_ver}] - {today}\n\nThis is a service update that focuses on package dependencies.\n\n" + idx = content.find("## [") + content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry) + print(f"Added CHANGELOG entry for v{new_ver}") + with open("CHANGELOG.md", "w", encoding="utf-8") as f: f.write(content) - print(f"CHANGELOG updated for v{new_ver}") EOF env: NEW_VERSION: ${{ steps.newver.outputs.new }} From b8470f224ef5f90bade8a45e15e0ff37108c0eab Mon Sep 17 00:00:00 2001 From: gimlichael Date: Thu, 22 Oct 2026 18:02:53 +0200 Subject: [PATCH 05/20] =?UTF-8?q?=F0=9F=8E=A8=20remove=20byte=20order=20ma?= =?UTF-8?q?rks=20and=20trailing=20whitespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strips UTF-8 byte order marks and trailing whitespace from the C# files so the repository matches the .editorconfig formatting rules. No code changes; the diff is formatting only. --- .../BenchmarkContext.cs | 44 +- .../BenchmarkProgram.cs | 2 +- .../BenchmarkWorkspace.cs | 450 ++-- .../BenchmarkWorkspaceOptions.cs | 684 +++--- .../BenchmarkWorkspaceOptionsExtensions.cs | 2 +- .../GlobalSuppressions.cs | 16 +- .../IBenchmarkWorkspace.cs | 40 +- .../ServiceCollectionExtensions.cs | 4 +- .../BenchmarkProgramTest.cs | 800 +++---- .../BenchmarkContextTest.cs | 446 ++-- .../BenchmarkProgramTest.cs | 784 +++---- .../BenchmarkProgramTest.cs | 290 +-- .../BenchmarkWorkspaceOptionsTest.cs | 1478 ++++++------ .../BenchmarkWorkspaceTest.cs | 2002 ++++++++--------- .../Program.cs | 2 +- .../BenchmarkWorkspaceBenchmark.cs | 144 +- .../BenchmarkWorkspaceOptionsBenchmark.cs | 228 +- .../BenchmarkContextBenchmark.cs | 148 +- .../BenchmarkProgramBenchmark.cs | 120 +- 19 files changed, 3842 insertions(+), 3842 deletions(-) diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs index 4942782..00d6747 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs @@ -1,22 +1,22 @@ -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -/// -/// Represents the command-line context for a benchmark run. -/// -public class BenchmarkContext -{ - /// - /// Initializes a new instance of the class. - /// - /// The command-line arguments passed to the . - public BenchmarkContext(string[] args) - { - Args = args ?? []; - } - - /// - /// Gets the command-line arguments passed to the . - /// - /// The command-line arguments. - public string[] Args { get; } -} +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +/// +/// Represents the command-line context for a benchmark run. +/// +public class BenchmarkContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The command-line arguments passed to the . + public BenchmarkContext(string[] args) + { + Args = args ?? []; + } + + /// + /// Gets the command-line arguments passed to the . + /// + /// The command-line arguments. + public string[] Args { get; } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs index 17d18f2..4292ee1 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs @@ -1,4 +1,4 @@ -using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Filters; using BenchmarkDotNet.Running; using Codebelt.Bootstrapper.Console; diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs index ca6932d..1f2e5d9 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs @@ -1,225 +1,225 @@ -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Toolchains.InProcess.Emit; -using Cuemon; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -/// -/// Provides a default implementation of for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet. -/// -public sealed class BenchmarkWorkspace : IBenchmarkWorkspace -{ - private readonly BenchmarkWorkspaceOptions _options; - private static bool _assemblyResolverRegistered; - private static readonly Lock AssemblyResolverLock = new(); - private static Dictionary _assemblyLookup = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Gets the path to the BenchmarkDotNet results directory - /// - /// The which configures repository paths, build modes and BenchmarkDotNet configuration. - /// A string containing the full path to the 'results' directory within the artifacts path. - /// - /// This path is constructed from the configured and the "results" subdirectory. - /// - public static string GetReportsResultsPath(BenchmarkWorkspaceOptions options) - { - Validator.ThrowIfInvalidOptions(options); - return Path.Combine(options.Configuration.ArtifactsPath, "results"); - } - - /// - /// Gets the path to the tuning folder where final benchmark reports are stored. - /// - /// The which configures repository paths, build modes and BenchmarkDotNet configuration. - /// A string representing the full path to the tuning reports directory. - /// - /// This path is constructed from the configured and the . - /// - public static string GetReportsTuningPath(BenchmarkWorkspaceOptions options) - { - Validator.ThrowIfInvalidOptions(options); - return Path.Combine(options.Configuration.ArtifactsPath, options.RepositoryTuningFolder); - } - - /// - /// Initializes a new instance of the class with the specified options. - /// - /// The which configures repository paths, build modes and BenchmarkDotNet configuration. - /// - /// cannot be null. - /// - /// - /// are not in a valid state. - /// - public BenchmarkWorkspace(BenchmarkWorkspaceOptions options) - { - Validator.ThrowIfInvalidOptions(options); - if (options.AllowDebugBuild && options.Configuration is ManualConfig mc) - { - mc.Options |= ConfigOptions.DisableOptimizationsValidator; - options.Configuration = mc.AddJob(Job.Default.WithToolchain(new InProcessEmitToolchain(TimeSpan.FromHours(1), true))); - } - _options = options; - } - - /// - /// Loads benchmark assemblies discovered recursively in the tuning folder. - /// - /// An array of instances representing the loaded benchmark assemblies. - /// - /// Assemblies are selected by matching the configured , - /// the build configuration (Debug/Release based on ), - /// and the target framework moniker (). - /// - /// - /// Thrown when no matching assemblies could be loaded from the tuning folder. Ensure the tuning folder contains - /// built benchmark assemblies for the configured build configuration and TFM. - /// - public Assembly[] LoadBenchmarkAssemblies() - { - var useDebugBuild = _options.AllowDebugBuild; - return LoadAssemblies( - _options.RepositoryPath, - _options.TargetFrameworkMoniker, - _options.BenchmarkProjectSuffix, - _options.RepositoryTuningFolder, - useDebugBuild).ToArray(); - } - - /// - /// Performs post-processing of artifacts produced by BenchmarkDotNet. - /// - /// - /// This method moves files found in the BenchmarkDotNet artifacts "results" directory into the tuning folder under the configured artifacts path and then deletes the now-empty "results" directory. - /// - public void PostProcessArtifacts() - { - CleanupResults(GetReportsResultsPath(_options), GetReportsTuningPath(_options)); - } - - private static IEnumerable LoadAssemblies(string repositoryPath, string targetFrameworkMoniker, string benchmarkProjectSuffix, string repositoryTuningFolder, bool useDebugBuild) - { - var tuningDir = Path.Combine(repositoryPath, repositoryTuningFolder); - Directory.CreateDirectory(tuningDir); - - UpdateAssemblyLookup(tuningDir); - EnsureAssemblyResolverRegistered(); - - var debugOrRelease = useDebugBuild ? "Debug" : "Release"; - var buildSegment = Path.Combine("bin", debugOrRelease, targetFrameworkMoniker); - - var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies(); - var assemblies = new List(); - - var candidatePaths = Directory - .EnumerateFiles(tuningDir, $"*.{benchmarkProjectSuffix}.dll", SearchOption.AllDirectories) - .Where(path => path.Contains(buildSegment, StringComparison.OrdinalIgnoreCase)); - - foreach (var path in candidatePaths) - { - try - { - var candidateName = AssemblyName.GetAssemblyName(path); - var existing = alreadyLoaded.FirstOrDefault(a => - AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName)); - - if (existing != null) - { - assemblies.Add(existing); - continue; - } - - if (assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName))) - { - continue; - } - - assemblies.Add(Assembly.LoadFrom(path)); - } - catch - { - // swallow and continue - } - } - - if (assemblies.Count == 0) - { - throw new InvalidOperationException($"No assemblies were loaded. Ensure that '{tuningDir}' contains assemblies for '{debugOrRelease}' build and '{targetFrameworkMoniker}' moniker."); - } - - return assemblies; - } - - private static void UpdateAssemblyLookup(string tuningDir) - { - var allDlls = Directory.EnumerateFiles(tuningDir, "*.dll", SearchOption.AllDirectories); - var map = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var path in allDlls) - { - var simpleName = Path.GetFileNameWithoutExtension(path); - if (string.IsNullOrEmpty(simpleName)) - { - continue; - } - map[simpleName] = path; - } - _assemblyLookup = map; - } - - private static void EnsureAssemblyResolverRegistered() - { - lock (AssemblyResolverLock) - { - if (_assemblyResolverRegistered) { return; } - - AppDomain.CurrentDomain.AssemblyResolve += (_, args) => - { - try - { - var requestedName = new AssemblyName(args.Name).Name; - if (string.IsNullOrEmpty(requestedName)) { return null; } - - if (!_assemblyLookup.TryGetValue(requestedName, out var path) || !File.Exists(path)) - { - return null; - } - - var asmName = AssemblyName.GetAssemblyName(path); - var already = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), asmName)); - if (already != null) { return already; } - - return Assembly.LoadFrom(path); - } - catch - { - return null; - } - }; - - _assemblyResolverRegistered = true; - } - } - - private static void CleanupResults(string reportsResultsPath, string reportsTuningPath) - { - if (!Directory.Exists(reportsResultsPath)) { return; } - - Directory.CreateDirectory(reportsTuningPath); - - foreach (var file in Directory.GetFiles(reportsResultsPath).Where(f => !Path.GetFileNameWithoutExtension(f).StartsWith("BenchmarkRun", StringComparison.Ordinal))) - { - var targetFile = Path.Combine(reportsTuningPath, Path.GetFileName(file)); - File.Move(file, targetFile, true); - } - - Directory.Delete(reportsResultsPath, recursive: true); - } -} +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using Cuemon; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Provides a default implementation of for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet. +/// +public sealed class BenchmarkWorkspace : IBenchmarkWorkspace +{ + private readonly BenchmarkWorkspaceOptions _options; + private static bool _assemblyResolverRegistered; + private static readonly Lock AssemblyResolverLock = new(); + private static Dictionary _assemblyLookup = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets the path to the BenchmarkDotNet results directory + /// + /// The which configures repository paths, build modes and BenchmarkDotNet configuration. + /// A string containing the full path to the 'results' directory within the artifacts path. + /// + /// This path is constructed from the configured and the "results" subdirectory. + /// + public static string GetReportsResultsPath(BenchmarkWorkspaceOptions options) + { + Validator.ThrowIfInvalidOptions(options); + return Path.Combine(options.Configuration.ArtifactsPath, "results"); + } + + /// + /// Gets the path to the tuning folder where final benchmark reports are stored. + /// + /// The which configures repository paths, build modes and BenchmarkDotNet configuration. + /// A string representing the full path to the tuning reports directory. + /// + /// This path is constructed from the configured and the . + /// + public static string GetReportsTuningPath(BenchmarkWorkspaceOptions options) + { + Validator.ThrowIfInvalidOptions(options); + return Path.Combine(options.Configuration.ArtifactsPath, options.RepositoryTuningFolder); + } + + /// + /// Initializes a new instance of the class with the specified options. + /// + /// The which configures repository paths, build modes and BenchmarkDotNet configuration. + /// + /// cannot be null. + /// + /// + /// are not in a valid state. + /// + public BenchmarkWorkspace(BenchmarkWorkspaceOptions options) + { + Validator.ThrowIfInvalidOptions(options); + if (options.AllowDebugBuild && options.Configuration is ManualConfig mc) + { + mc.Options |= ConfigOptions.DisableOptimizationsValidator; + options.Configuration = mc.AddJob(Job.Default.WithToolchain(new InProcessEmitToolchain(TimeSpan.FromHours(1), true))); + } + _options = options; + } + + /// + /// Loads benchmark assemblies discovered recursively in the tuning folder. + /// + /// An array of instances representing the loaded benchmark assemblies. + /// + /// Assemblies are selected by matching the configured , + /// the build configuration (Debug/Release based on ), + /// and the target framework moniker (). + /// + /// + /// Thrown when no matching assemblies could be loaded from the tuning folder. Ensure the tuning folder contains + /// built benchmark assemblies for the configured build configuration and TFM. + /// + public Assembly[] LoadBenchmarkAssemblies() + { + var useDebugBuild = _options.AllowDebugBuild; + return LoadAssemblies( + _options.RepositoryPath, + _options.TargetFrameworkMoniker, + _options.BenchmarkProjectSuffix, + _options.RepositoryTuningFolder, + useDebugBuild).ToArray(); + } + + /// + /// Performs post-processing of artifacts produced by BenchmarkDotNet. + /// + /// + /// This method moves files found in the BenchmarkDotNet artifacts "results" directory into the tuning folder under the configured artifacts path and then deletes the now-empty "results" directory. + /// + public void PostProcessArtifacts() + { + CleanupResults(GetReportsResultsPath(_options), GetReportsTuningPath(_options)); + } + + private static IEnumerable LoadAssemblies(string repositoryPath, string targetFrameworkMoniker, string benchmarkProjectSuffix, string repositoryTuningFolder, bool useDebugBuild) + { + var tuningDir = Path.Combine(repositoryPath, repositoryTuningFolder); + Directory.CreateDirectory(tuningDir); + + UpdateAssemblyLookup(tuningDir); + EnsureAssemblyResolverRegistered(); + + var debugOrRelease = useDebugBuild ? "Debug" : "Release"; + var buildSegment = Path.Combine("bin", debugOrRelease, targetFrameworkMoniker); + + var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies(); + var assemblies = new List(); + + var candidatePaths = Directory + .EnumerateFiles(tuningDir, $"*.{benchmarkProjectSuffix}.dll", SearchOption.AllDirectories) + .Where(path => path.Contains(buildSegment, StringComparison.OrdinalIgnoreCase)); + + foreach (var path in candidatePaths) + { + try + { + var candidateName = AssemblyName.GetAssemblyName(path); + var existing = alreadyLoaded.FirstOrDefault(a => + AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName)); + + if (existing != null) + { + assemblies.Add(existing); + continue; + } + + if (assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName))) + { + continue; + } + + assemblies.Add(Assembly.LoadFrom(path)); + } + catch + { + // swallow and continue + } + } + + if (assemblies.Count == 0) + { + throw new InvalidOperationException($"No assemblies were loaded. Ensure that '{tuningDir}' contains assemblies for '{debugOrRelease}' build and '{targetFrameworkMoniker}' moniker."); + } + + return assemblies; + } + + private static void UpdateAssemblyLookup(string tuningDir) + { + var allDlls = Directory.EnumerateFiles(tuningDir, "*.dll", SearchOption.AllDirectories); + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var path in allDlls) + { + var simpleName = Path.GetFileNameWithoutExtension(path); + if (string.IsNullOrEmpty(simpleName)) + { + continue; + } + map[simpleName] = path; + } + _assemblyLookup = map; + } + + private static void EnsureAssemblyResolverRegistered() + { + lock (AssemblyResolverLock) + { + if (_assemblyResolverRegistered) { return; } + + AppDomain.CurrentDomain.AssemblyResolve += (_, args) => + { + try + { + var requestedName = new AssemblyName(args.Name).Name; + if (string.IsNullOrEmpty(requestedName)) { return null; } + + if (!_assemblyLookup.TryGetValue(requestedName, out var path) || !File.Exists(path)) + { + return null; + } + + var asmName = AssemblyName.GetAssemblyName(path); + var already = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), asmName)); + if (already != null) { return already; } + + return Assembly.LoadFrom(path); + } + catch + { + return null; + } + }; + + _assemblyResolverRegistered = true; + } + } + + private static void CleanupResults(string reportsResultsPath, string reportsTuningPath) + { + if (!Directory.Exists(reportsResultsPath)) { return; } + + Directory.CreateDirectory(reportsTuningPath); + + foreach (var file in Directory.GetFiles(reportsResultsPath).Where(f => !Path.GetFileNameWithoutExtension(f).StartsWith("BenchmarkRun", StringComparison.Ordinal))) + { + var targetFile = Path.Combine(reportsTuningPath, Path.GetFileName(file)); + File.Move(file, targetFile, true); + } + + Directory.Delete(reportsResultsPath, recursive: true); + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs index 8dfec54..1bf80f9 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs @@ -1,343 +1,343 @@ -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Exporters; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Loggers; -using BenchmarkDotNet.Reports; -using Cuemon; -using Cuemon.Configuration; -using Perfolizer.Horology; -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.Versioning; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -/// -/// Configuration options for . -/// -/// -/// The following table shows the initial property values for an instance of . -/// -/// -/// Property -/// Initial Value -/// -/// -/// -/// Resolved runtime by filesystem, e.g. C:\Repos\MyBenchmarkRepo. -/// -/// -/// -/// BenchmarkDotNet configured to use recommended settings as outlined in: https://github.com/dotnet/performance/blob/main/src/harness/BenchmarkDotNet.Extensions/RecommendedConfig.cs. -/// -/// -/// -/// Resolved runtime by reflection, e.g. net10.0. -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// false -/// -/// -/// -/// false -/// -/// -/// -public class BenchmarkWorkspaceOptions : IValidatableParameterObject, IPostConfigurableParameterObject -{ - /// - /// The default folder name where benchmark reports are written relative to the repository path. - /// - public const string DefaultRepositoryReportsFolder = "reports"; - - /// - /// The default folder name used for tuning artifacts relative to the repository path. - /// - public const string DefaultRepositoryTuningFolder = "tuning"; - - /// - /// The default suffix used to identify benchmark projects. - /// - public const string DefaultBenchmarkProjectSuffix = "Benchmarks"; - - /// - /// A tuned preset that serves as a fast, reliable baseline for most benchmarks, balancing measurement accuracy with developer efficiency. - /// - /// - /// A configured with reduced warmup, shortened iteration duration, controlled iteration counts, and without system power plan enforcement. - /// - /// - /// Based on the recommended configuration used in the .NET Performance repository: https://github.com/dotnet/performance/blob/main/src/harness/BenchmarkDotNet.Extensions/RecommendedConfig.cs - /// - public static readonly Job Slim = GetDefaultConfiguredJob(); - - private static readonly string DefaultRepositoryPath = GetDefaultRepositoryPath(); - private static readonly string DefaultTargetFrameworkMoniker = ResolveCurrentTfm(); - - /// - /// Initializes a new instance of the class with sensible defaults. - /// - public BenchmarkWorkspaceOptions() - { - Configuration = GetDefaultConfiguration(); - RepositoryPath = DefaultRepositoryPath; - TargetFrameworkMoniker = DefaultTargetFrameworkMoniker; - RepositoryTuningFolder = DefaultRepositoryTuningFolder; - RepositoryReportsFolder = DefaultRepositoryReportsFolder; - BenchmarkProjectSuffix = DefaultBenchmarkProjectSuffix; - } - - /// - /// Gets or sets the root path of the repository where benchmark projects and reports live. - /// - /// The repository root path. - public string RepositoryPath { get; set; } - - /// - /// Gets or sets the instance used by BenchmarkDotNet. - /// - /// The BenchmarkDotNet configuration. - public IConfig Configuration { get; set; } - - /// - /// Gets or sets the target framework moniker (TFM) string to be used for benchmark discovery and reporting. - /// - /// The target framework moniker to run benchmarks for, e.g. net10.0. - public string TargetFrameworkMoniker { get; set; } - - /// - /// Gets or sets the folder name under used to store tuning artifacts. - /// - /// The tuning folder name. Defaults to . - public string RepositoryTuningFolder { get; set; } - - /// - /// Gets or sets the folder name under used to store generated reports. - /// - /// The reports folder name. Defaults to . - public string RepositoryReportsFolder { get; set; } - - /// - /// Gets or sets the project name suffix used to identify benchmark projects. - /// - /// - /// The benchmark project suffix. Defaults to . - /// - public string BenchmarkProjectSuffix { get; set; } - - /// - /// Gets or sets a value indicating whether the benchmarks should be allowed to run using a Debug build. - /// - /// - /// true to allow Debug builds for benchmarks; otherwise, false. Default is false. - /// - public bool AllowDebugBuild { get; set; } - - /// - /// Gets or sets a value indicating whether benchmarks that already have generated reports should be skipped during execution. - /// - /// - /// true to skip benchmarks with existing reports; otherwise, false. Default is false. - /// - public bool SkipBenchmarksWithReports { get; set; } - - /// - /// Finalizes the configured options before use. - /// - /// This method updates the to set the BenchmarkDotNet artifacts path to the combination of and . - public void PostConfigureOptions() - { - if (string.IsNullOrWhiteSpace(Configuration.ArtifactsPath)) - { - var artifactsPath = Path.Combine(RepositoryPath, RepositoryReportsFolder); - - if (Configuration is ManualConfig manual) - { - manual.ArtifactsPath = artifactsPath; - } - else - { - Configuration = Configuration.WithArtifactsPath(artifactsPath); - } - } - } - - /// - /// Determines whether the public read-write properties of this instance are in a valid state. - /// - /// - /// cannot be . - /// -or- - /// cannot be , empty or consist only of white-space characters. - /// -or- - /// cannot be , empty or consist only of white-space characters. - /// -or- - /// cannot be , empty or consist only of white-space characters. - /// -or- - /// cannot be , empty or consist only of white-space characters. - /// -or- - /// cannot be , empty or consist only of white-space characters. - /// - public void ValidateOptions() - { - Validator.ThrowIfInvalidState(Configuration == null); - Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryPath)); - Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryTuningFolder)); - Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryReportsFolder)); - Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(TargetFrameworkMoniker)); - Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(BenchmarkProjectSuffix)); - } - - private static string GetDefaultRepositoryPath() - { - var dir = AppContext.BaseDirectory; - while (!string.IsNullOrWhiteSpace(dir)) - { - if (Directory.Exists(Path.Combine(dir, ".git"))) - { - return dir; - } - dir = Path.GetDirectoryName(dir); - } - return Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."); - } - - private static ManualConfig GetDefaultConfiguration() - { - var config = ManualConfig.CreateEmpty() - .WithBuildTimeout(TimeSpan.FromMinutes(15)) // for slow machines - .AddLogger(ConsoleLogger.Default) // log output to console - .AddValidator(DefaultConfig.Instance.GetValidators().ToArray()) // copy default validators - .AddAnalyser(DefaultConfig.Instance.GetAnalysers().ToArray()) // copy default analysers - .AddExporter(MarkdownExporter.GitHub) // export to GitHub markdown - .AddColumnProvider(DefaultColumnProviders.Instance) // display default columns (method name, args etc) - .AddJob(Slim.AsDefault()) // tell BDN that this are our default settings - .AddDiagnoser(MemoryDiagnoser.Default) // MemoryDiagnoser is enabled by default - .AddColumn(StatisticColumn.Median, StatisticColumn.Min, StatisticColumn.Max) - .WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(36)); // the default is 20 and trims too aggressively some benchmark results - config.Options = ConfigOptions.DisableLogFile; - return config; - } - - private static Job GetDefaultConfiguredJob() - { - return Job.Default - .WithWarmupCount(1) // 1 warmup is enough for our purpose - .WithIterationTime(TimeInterval.FromMilliseconds(250)) // the default is 0.5s per iteration, which is slightly too much for us - .WithMinIterationCount(15) - .WithMaxIterationCount(20) // we don't want to run more than 20 iterations - .DontEnforcePowerPlan(); // make sure BDN does not try to enforce High Performance power plan on Windows - } - +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Reports; +using Cuemon; +using Cuemon.Configuration; +using Perfolizer.Horology; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Configuration options for . +/// +/// +/// The following table shows the initial property values for an instance of . +/// +/// +/// Property +/// Initial Value +/// +/// +/// +/// Resolved runtime by filesystem, e.g. C:\Repos\MyBenchmarkRepo. +/// +/// +/// +/// BenchmarkDotNet configured to use recommended settings as outlined in: https://github.com/dotnet/performance/blob/main/src/harness/BenchmarkDotNet.Extensions/RecommendedConfig.cs. +/// +/// +/// +/// Resolved runtime by reflection, e.g. net10.0. +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// false +/// +/// +/// +/// false +/// +/// +/// +public class BenchmarkWorkspaceOptions : IValidatableParameterObject, IPostConfigurableParameterObject +{ + /// + /// The default folder name where benchmark reports are written relative to the repository path. + /// + public const string DefaultRepositoryReportsFolder = "reports"; + + /// + /// The default folder name used for tuning artifacts relative to the repository path. + /// + public const string DefaultRepositoryTuningFolder = "tuning"; + + /// + /// The default suffix used to identify benchmark projects. + /// + public const string DefaultBenchmarkProjectSuffix = "Benchmarks"; + + /// + /// A tuned preset that serves as a fast, reliable baseline for most benchmarks, balancing measurement accuracy with developer efficiency. + /// + /// + /// A configured with reduced warmup, shortened iteration duration, controlled iteration counts, and without system power plan enforcement. + /// + /// + /// Based on the recommended configuration used in the .NET Performance repository: https://github.com/dotnet/performance/blob/main/src/harness/BenchmarkDotNet.Extensions/RecommendedConfig.cs + /// + public static readonly Job Slim = GetDefaultConfiguredJob(); + + private static readonly string DefaultRepositoryPath = GetDefaultRepositoryPath(); + private static readonly string DefaultTargetFrameworkMoniker = ResolveCurrentTfm(); + + /// + /// Initializes a new instance of the class with sensible defaults. + /// + public BenchmarkWorkspaceOptions() + { + Configuration = GetDefaultConfiguration(); + RepositoryPath = DefaultRepositoryPath; + TargetFrameworkMoniker = DefaultTargetFrameworkMoniker; + RepositoryTuningFolder = DefaultRepositoryTuningFolder; + RepositoryReportsFolder = DefaultRepositoryReportsFolder; + BenchmarkProjectSuffix = DefaultBenchmarkProjectSuffix; + } + + /// + /// Gets or sets the root path of the repository where benchmark projects and reports live. + /// + /// The repository root path. + public string RepositoryPath { get; set; } + + /// + /// Gets or sets the instance used by BenchmarkDotNet. + /// + /// The BenchmarkDotNet configuration. + public IConfig Configuration { get; set; } + + /// + /// Gets or sets the target framework moniker (TFM) string to be used for benchmark discovery and reporting. + /// + /// The target framework moniker to run benchmarks for, e.g. net10.0. + public string TargetFrameworkMoniker { get; set; } + + /// + /// Gets or sets the folder name under used to store tuning artifacts. + /// + /// The tuning folder name. Defaults to . + public string RepositoryTuningFolder { get; set; } + + /// + /// Gets or sets the folder name under used to store generated reports. + /// + /// The reports folder name. Defaults to . + public string RepositoryReportsFolder { get; set; } + + /// + /// Gets or sets the project name suffix used to identify benchmark projects. + /// + /// + /// The benchmark project suffix. Defaults to . + /// + public string BenchmarkProjectSuffix { get; set; } + + /// + /// Gets or sets a value indicating whether the benchmarks should be allowed to run using a Debug build. + /// + /// + /// true to allow Debug builds for benchmarks; otherwise, false. Default is false. + /// + public bool AllowDebugBuild { get; set; } + + /// + /// Gets or sets a value indicating whether benchmarks that already have generated reports should be skipped during execution. + /// + /// + /// true to skip benchmarks with existing reports; otherwise, false. Default is false. + /// + public bool SkipBenchmarksWithReports { get; set; } + + /// + /// Finalizes the configured options before use. + /// + /// This method updates the to set the BenchmarkDotNet artifacts path to the combination of and . + public void PostConfigureOptions() + { + if (string.IsNullOrWhiteSpace(Configuration.ArtifactsPath)) + { + var artifactsPath = Path.Combine(RepositoryPath, RepositoryReportsFolder); + + if (Configuration is ManualConfig manual) + { + manual.ArtifactsPath = artifactsPath; + } + else + { + Configuration = Configuration.WithArtifactsPath(artifactsPath); + } + } + } + + /// + /// Determines whether the public read-write properties of this instance are in a valid state. + /// + /// + /// cannot be . + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// + public void ValidateOptions() + { + Validator.ThrowIfInvalidState(Configuration == null); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryPath)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryTuningFolder)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryReportsFolder)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(TargetFrameworkMoniker)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(BenchmarkProjectSuffix)); + } + + private static string GetDefaultRepositoryPath() + { + var dir = AppContext.BaseDirectory; + while (!string.IsNullOrWhiteSpace(dir)) + { + if (Directory.Exists(Path.Combine(dir, ".git"))) + { + return dir; + } + dir = Path.GetDirectoryName(dir); + } + return Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."); + } + + private static ManualConfig GetDefaultConfiguration() + { + var config = ManualConfig.CreateEmpty() + .WithBuildTimeout(TimeSpan.FromMinutes(15)) // for slow machines + .AddLogger(ConsoleLogger.Default) // log output to console + .AddValidator(DefaultConfig.Instance.GetValidators().ToArray()) // copy default validators + .AddAnalyser(DefaultConfig.Instance.GetAnalysers().ToArray()) // copy default analysers + .AddExporter(MarkdownExporter.GitHub) // export to GitHub markdown + .AddColumnProvider(DefaultColumnProviders.Instance) // display default columns (method name, args etc) + .AddJob(Slim.AsDefault()) // tell BDN that this are our default settings + .AddDiagnoser(MemoryDiagnoser.Default) // MemoryDiagnoser is enabled by default + .AddColumn(StatisticColumn.Median, StatisticColumn.Min, StatisticColumn.Max) + .WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(36)); // the default is 20 and trims too aggressively some benchmark results + config.Options = ConfigOptions.DisableLogFile; + return config; + } + + private static Job GetDefaultConfiguredJob() + { + return Job.Default + .WithWarmupCount(1) // 1 warmup is enough for our purpose + .WithIterationTime(TimeInterval.FromMilliseconds(250)) // the default is 0.5s per iteration, which is slightly too much for us + .WithMinIterationCount(15) + .WithMaxIterationCount(20) // we don't want to run more than 20 iterations + .DontEnforcePowerPlan(); // make sure BDN does not try to enforce High Performance power plan on Windows + } + private static string ParseTargetFrameworkMoniker(string frameworkName) - { - if (string.IsNullOrEmpty(frameworkName)) - { - return null; - } - - try - { - var fn = new FrameworkName(frameworkName); - var v = fn.Version; - - // .NET Framework → net11, net20, net35, net40, net403, net45, net451, ..., net48, net481 - if (fn.Identifier.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase)) - { - // Base: net4{minor} or net{major}{minor} for older ones - var tfm = $"net{v.Major}{v.Minor}"; - - // For 4.x: append Build when present (4.0.3 → net403, 4.5.1 → net451, 4.8.1 → net481) - if (v.Major >= 4 && v.Build > 0) - { - tfm += v.Build; - } - - return tfm; - } - - // .NET Standard → netstandard1.0–2.1 - if (fn.Identifier.Equals(".NETStandard", StringComparison.OrdinalIgnoreCase)) - { - return $"netstandard{v.Major}.{v.Minor}"; - } - - // .NET Core / .NET (CoreApp) - if (fn.Identifier.Equals(".NETCoreApp", StringComparison.OrdinalIgnoreCase)) - { - // .NET Core 1.0–3.1 use netcoreappX.Y - if (v.Major <= 3) - { - return $"netcoreapp{v.Major}.{v.Minor}"; - } - - // .NET 5+ uses netX.Y - return $"net{v.Major}.{v.Minor}"; - } - } - catch - { - // ignore invalid framework names - } - - return null; - } - - private static string ResolveCurrentTfm() - { - try - { - var entry = Assembly.GetEntryAssembly(); - var tfa = entry?.GetCustomAttribute(); - if (!string.IsNullOrEmpty(tfa?.FrameworkName)) - { - var result = ParseTargetFrameworkMoniker(tfa.FrameworkName); - if (result != null) - { - return result; - } - } - } - catch - { - // ignore and fallback to dir inspection - } - - try - { - var baseDir = AppContext.BaseDirectory; - var dir = new DirectoryInfo(baseDir); - while (dir != null) - { - if (dir.Name.StartsWith("net", StringComparison.OrdinalIgnoreCase)) - { - return dir.Name; - } - dir = dir.Parent; - } - } - catch - { - // ignore and fallback to null - } - - return null; - } -} + { + if (string.IsNullOrEmpty(frameworkName)) + { + return null; + } + + try + { + var fn = new FrameworkName(frameworkName); + var v = fn.Version; + + // .NET Framework → net11, net20, net35, net40, net403, net45, net451, ..., net48, net481 + if (fn.Identifier.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase)) + { + // Base: net4{minor} or net{major}{minor} for older ones + var tfm = $"net{v.Major}{v.Minor}"; + + // For 4.x: append Build when present (4.0.3 → net403, 4.5.1 → net451, 4.8.1 → net481) + if (v.Major >= 4 && v.Build > 0) + { + tfm += v.Build; + } + + return tfm; + } + + // .NET Standard → netstandard1.0–2.1 + if (fn.Identifier.Equals(".NETStandard", StringComparison.OrdinalIgnoreCase)) + { + return $"netstandard{v.Major}.{v.Minor}"; + } + + // .NET Core / .NET (CoreApp) + if (fn.Identifier.Equals(".NETCoreApp", StringComparison.OrdinalIgnoreCase)) + { + // .NET Core 1.0–3.1 use netcoreappX.Y + if (v.Major <= 3) + { + return $"netcoreapp{v.Major}.{v.Minor}"; + } + + // .NET 5+ uses netX.Y + return $"net{v.Major}.{v.Minor}"; + } + } + catch + { + // ignore invalid framework names + } + + return null; + } + + private static string ResolveCurrentTfm() + { + try + { + var entry = Assembly.GetEntryAssembly(); + var tfa = entry?.GetCustomAttribute(); + if (!string.IsNullOrEmpty(tfa?.FrameworkName)) + { + var result = ParseTargetFrameworkMoniker(tfa.FrameworkName); + if (result != null) + { + return result; + } + } + } + catch + { + // ignore and fallback to dir inspection + } + + try + { + var baseDir = AppContext.BaseDirectory; + var dir = new DirectoryInfo(baseDir); + while (dir != null) + { + if (dir.Name.StartsWith("net", StringComparison.OrdinalIgnoreCase)) + { + return dir.Name; + } + dir = dir.Parent; + } + } + catch + { + // ignore and fallback to null + } + + return null; + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs index d4e3446..c164229 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using BenchmarkDotNet.Configs; using Cuemon; diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs index 69798b7..17f18a5 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs @@ -1,8 +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}")] +// 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/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs index b340c46..753176a 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs @@ -1,20 +1,20 @@ -using System.Reflection; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -/// -/// Defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet. -/// -public interface IBenchmarkWorkspace -{ - /// - /// Loads assemblies that contain BenchmarkDotNet benchmarks. - /// - /// An array of instances representing the loaded benchmark assemblies. - Assembly[] LoadBenchmarkAssemblies(); - - /// - /// Performs post-processing on artifacts produced by BenchmarkDotNet. - /// - void PostProcessArtifacts(); -} +using System.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet. +/// +public interface IBenchmarkWorkspace +{ + /// + /// Loads assemblies that contain BenchmarkDotNet benchmarks. + /// + /// An array of instances representing the loaded benchmark assemblies. + Assembly[] LoadBenchmarkAssemblies(); + + /// + /// Performs post-processing on artifacts produced by BenchmarkDotNet. + /// + void PostProcessArtifacts(); +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs index 6035161..944b75c 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using Cuemon; using Microsoft.Extensions.DependencyInjection; @@ -38,7 +38,7 @@ public static IServiceCollection AddBenchmarkWorkspace(this IService Validator.ThrowIfInvalidConfigurator(setup, out var options); return services .AddSingleton() - .Configure(setup ?? (_ => {})) + .Configure(setup ?? (_ => { })) .AddSingleton(options); } } diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.FunctionalTests/BenchmarkProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.FunctionalTests/BenchmarkProgramTest.cs index 36c23ec..0858b36 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Console.FunctionalTests/BenchmarkProgramTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.FunctionalTests/BenchmarkProgramTest.cs @@ -1,356 +1,356 @@ -using BenchmarkDotNet.Configs; -using Codebelt.Extensions.Xunit; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.IO; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + /// /// Functional tests for that exercise the full hosted-application lifecycle /// including , , /// and the benchmark-filter pipeline (SkipBenchmarksWithReports). /// public class BenchmarkProgramTest : Test -{ - public BenchmarkProgramTest(ITestOutputHelper output) : base(output) - { - } - - // --------------------------------------------------------------------------- - // RunAsync – direct instantiation tests (avoid starting the full host) - // --------------------------------------------------------------------------- - - [Fact] - public async Task RunAsync_ShouldComplete_WhenWorkspaceReturnsNoAssemblies_AndArgsAreEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var workspace = new EmptyWorkspace(); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act – empty assemblies + empty args → empty foreach → no BenchmarkRunner call - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - Assert.True(workspace.PostProcessArtifactsCalled, "PostProcessArtifacts should always be called in the finally block."); - - TestOutput.WriteLine("RunAsync completed cleanly with empty workspace and no args."); - } - - [Fact] - public async Task RunAsync_ShouldUseBenchmarkSwitcher_WhenArgsAreNonEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var workspace = new EmptyWorkspace(); - - // "--list flat" asks BenchmarkSwitcher to list (not run) benchmarks; safe with an empty assembly list. - var context = new BenchmarkContext(new[] { "--list", "flat" }); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act – BenchmarkSwitcher path (context.Args.Length > 0) - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - Assert.True(workspace.PostProcessArtifactsCalled); - - TestOutput.WriteLine("RunAsync completed cleanly via BenchmarkSwitcher path."); - } - - [Fact] - public async Task RunAsync_ShouldSkipFiltering_WhenSkipBenchmarksWithReportsIsFalse() - { - // Arrange - var options = new BenchmarkWorkspaceOptions { SkipBenchmarksWithReports = false }; - var workspace = new EmptyWorkspace(); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert – no filtering attempted, completes normally - Assert.Null(exception); - } - - [Fact] - public async Task RunAsync_ShouldApplyFilters_WhenSkipBenchmarksWithReportsIsTrue_AndTuningDirDoesNotExist() - { - // Arrange – tuning dir does not exist → ApplyReportFilters early-returns - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var config = ManualConfig.CreateEmpty().WithArtifactsPath(Path.Combine(tempPath, "artifacts")); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning", - SkipBenchmarksWithReports = true - }; - - // Return the functional-test assembly so benchmarkTypes is populated with KnownBenchmark - var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act – tuning dir absent → ApplyReportFilters returns config unchanged - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } - - [Fact] - public async Task RunAsync_ShouldApplyFilters_WhenSkipBenchmarksWithReportsIsTrue_AndMatchingReportExists() - { - // Arrange – a report file whose name contains "KnownBenchmark" exists in the tuning dir. - // KnownBenchmark (defined below) is in this assembly so it will be found by benchmarkTypes. - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - Directory.CreateDirectory(tuningDir); - - // Report file whose name encodes the KnownBenchmark type - File.WriteAllText( - Path.Combine(tuningDir, "Codebelt.Extensions.BenchmarkDotNet.Console.KnownBenchmark-report-github.md"), - "# benchmark report"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning", - SkipBenchmarksWithReports = true - }; - - var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act – filter IS applied for KnownBenchmark (matchingType != null path) - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - - TestOutput.WriteLine("Filter applied for KnownBenchmark report."); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } - - [Fact] - public async Task RunAsync_ShouldSkipReport_WhenFilenameHasLeadingDash() - { - // Arrange – a file whose name starts with "-" causes Split('-').FirstOrDefault() == "" - // which exercises the first 'return null' branch in FindMatchingBenchmarkType. - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - Directory.CreateDirectory(tuningDir); - - File.WriteAllText(Path.Combine(tuningDir, "-leading-dash-report.md"), "edge case"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning", - SkipBenchmarksWithReports = true - }; - - var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert – no filter applied (FindMatchingBenchmarkType returned null), no exception - Assert.Null(exception); - - TestOutput.WriteLine("Leading-dash filename correctly produced null match."); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } - - [Fact] - public async Task RunAsync_ShouldSkipReport_WhenFilenameHasEmptyTypePart() - { - // Arrange – ".-something.md" → potentialTypeFullName = "." → Split('.').LastOrDefault() == "" - // which exercises the second 'return null' branch in FindMatchingBenchmarkType. - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - Directory.CreateDirectory(tuningDir); - - File.WriteAllText(Path.Combine(tuningDir, ".-empty-type-part.md"), "edge case"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning", - SkipBenchmarksWithReports = true - }; - - var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - - TestOutput.WriteLine("Dot-only type-name segment correctly produced null match."); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } - - [Fact] - public async Task RunAsync_ShouldSkipReport_WhenFilenameDoesNotMatchAnyBenchmarkType() - { - // Arrange – well-formed filename but no matching type in the assembly exercises - // the 'return null' path where FirstOrDefault returns null from benchmarkTypes. - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - Directory.CreateDirectory(tuningDir); - - File.WriteAllText( - Path.Combine(tuningDir, "SomeNamespace.NonExistentBenchmark-report.md"), - "no matching type"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning", - SkipBenchmarksWithReports = true - }; - - var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - - TestOutput.WriteLine("No matching benchmark type found – report correctly skipped."); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } - - // --------------------------------------------------------------------------- - // BenchmarkProgram.Run – full hosted-application lifecycle tests - // --------------------------------------------------------------------------- - - [Fact] - public void Run_WithFakeWorkspace_ShouldCompleteWithoutException() - { - // Exercises the static Run(string[], Action) overload (line 82) - // and the full 4-param generic Run (lines 97-105), which starts the host, calls RunAsync, - // and stops the host after RunAsync returns. - // Using named parameter 'setup' disambiguates from the 3-param overload. - var exception = Record.Exception(() => - BenchmarkProgram.Run(Array.Empty(), setup: (Action)null)); - - Assert.Null(exception); - TestOutput.WriteLine("Run completed without exception."); - } - - [Fact] - public void Run_WithFakeWorkspace_AndServiceConfigurator_ShouldInvokeConfigurator() - { - // Exercises Run(string[], Action, Action) - // and verifies that the serviceConfigurator delegate is invoked (line 102). - var configuratorWasCalled = false; - - var exception = Record.Exception(() => - BenchmarkProgram.Run( - Array.Empty(), - serviceConfigurator: services => - { - configuratorWasCalled = true; - services.AddSingleton(_ => null!); // harmless extra registration - })); - - Assert.Null(exception); - Assert.True(configuratorWasCalled, "serviceConfigurator should have been invoked during host setup."); - TestOutput.WriteLine("serviceConfigurator was invoked as expected."); - } - - [Fact] - public void Run_WithFakeWorkspace_AndNonEmptyArgs_ShouldUseBenchmarkSwitcherPath() - { - // Passing "--list flat" exercises the BenchmarkSwitcher branch in ExecuteBenchmarks. - // Using named parameter 'setup' disambiguates from the 3-param overload. - var exception = Record.Exception(() => - BenchmarkProgram.Run(new[] { "--list", "flat" }, setup: (Action)null)); - +{ + public BenchmarkProgramTest(ITestOutputHelper output) : base(output) + { + } + + // --------------------------------------------------------------------------- + // RunAsync – direct instantiation tests (avoid starting the full host) + // --------------------------------------------------------------------------- + + [Fact] + public async Task RunAsync_ShouldComplete_WhenWorkspaceReturnsNoAssemblies_AndArgsAreEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var workspace = new EmptyWorkspace(); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act – empty assemblies + empty args → empty foreach → no BenchmarkRunner call + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + Assert.True(workspace.PostProcessArtifactsCalled, "PostProcessArtifacts should always be called in the finally block."); + + TestOutput.WriteLine("RunAsync completed cleanly with empty workspace and no args."); + } + + [Fact] + public async Task RunAsync_ShouldUseBenchmarkSwitcher_WhenArgsAreNonEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var workspace = new EmptyWorkspace(); + + // "--list flat" asks BenchmarkSwitcher to list (not run) benchmarks; safe with an empty assembly list. + var context = new BenchmarkContext(new[] { "--list", "flat" }); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act – BenchmarkSwitcher path (context.Args.Length > 0) + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + Assert.True(workspace.PostProcessArtifactsCalled); + + TestOutput.WriteLine("RunAsync completed cleanly via BenchmarkSwitcher path."); + } + + [Fact] + public async Task RunAsync_ShouldSkipFiltering_WhenSkipBenchmarksWithReportsIsFalse() + { + // Arrange + var options = new BenchmarkWorkspaceOptions { SkipBenchmarksWithReports = false }; + var workspace = new EmptyWorkspace(); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert – no filtering attempted, completes normally + Assert.Null(exception); + } + + [Fact] + public async Task RunAsync_ShouldApplyFilters_WhenSkipBenchmarksWithReportsIsTrue_AndTuningDirDoesNotExist() + { + // Arrange – tuning dir does not exist → ApplyReportFilters early-returns + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var config = ManualConfig.CreateEmpty().WithArtifactsPath(Path.Combine(tempPath, "artifacts")); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning", + SkipBenchmarksWithReports = true + }; + + // Return the functional-test assembly so benchmarkTypes is populated with KnownBenchmark + var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act – tuning dir absent → ApplyReportFilters returns config unchanged + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } + + [Fact] + public async Task RunAsync_ShouldApplyFilters_WhenSkipBenchmarksWithReportsIsTrue_AndMatchingReportExists() + { + // Arrange – a report file whose name contains "KnownBenchmark" exists in the tuning dir. + // KnownBenchmark (defined below) is in this assembly so it will be found by benchmarkTypes. + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + Directory.CreateDirectory(tuningDir); + + // Report file whose name encodes the KnownBenchmark type + File.WriteAllText( + Path.Combine(tuningDir, "Codebelt.Extensions.BenchmarkDotNet.Console.KnownBenchmark-report-github.md"), + "# benchmark report"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning", + SkipBenchmarksWithReports = true + }; + + var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act – filter IS applied for KnownBenchmark (matchingType != null path) + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + + TestOutput.WriteLine("Filter applied for KnownBenchmark report."); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } + + [Fact] + public async Task RunAsync_ShouldSkipReport_WhenFilenameHasLeadingDash() + { + // Arrange – a file whose name starts with "-" causes Split('-').FirstOrDefault() == "" + // which exercises the first 'return null' branch in FindMatchingBenchmarkType. + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + Directory.CreateDirectory(tuningDir); + + File.WriteAllText(Path.Combine(tuningDir, "-leading-dash-report.md"), "edge case"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning", + SkipBenchmarksWithReports = true + }; + + var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert – no filter applied (FindMatchingBenchmarkType returned null), no exception + Assert.Null(exception); + + TestOutput.WriteLine("Leading-dash filename correctly produced null match."); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } + + [Fact] + public async Task RunAsync_ShouldSkipReport_WhenFilenameHasEmptyTypePart() + { + // Arrange – ".-something.md" → potentialTypeFullName = "." → Split('.').LastOrDefault() == "" + // which exercises the second 'return null' branch in FindMatchingBenchmarkType. + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + Directory.CreateDirectory(tuningDir); + + File.WriteAllText(Path.Combine(tuningDir, ".-empty-type-part.md"), "edge case"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning", + SkipBenchmarksWithReports = true + }; + + var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + + TestOutput.WriteLine("Dot-only type-name segment correctly produced null match."); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } + + [Fact] + public async Task RunAsync_ShouldSkipReport_WhenFilenameDoesNotMatchAnyBenchmarkType() + { + // Arrange – well-formed filename but no matching type in the assembly exercises + // the 'return null' path where FirstOrDefault returns null from benchmarkTypes. + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + Directory.CreateDirectory(tuningDir); + + File.WriteAllText( + Path.Combine(tuningDir, "SomeNamespace.NonExistentBenchmark-report.md"), + "no matching type"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning", + SkipBenchmarksWithReports = true + }; + + var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + + TestOutput.WriteLine("No matching benchmark type found – report correctly skipped."); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } + + // --------------------------------------------------------------------------- + // BenchmarkProgram.Run – full hosted-application lifecycle tests + // --------------------------------------------------------------------------- + + [Fact] + public void Run_WithFakeWorkspace_ShouldCompleteWithoutException() + { + // Exercises the static Run(string[], Action) overload (line 82) + // and the full 4-param generic Run (lines 97-105), which starts the host, calls RunAsync, + // and stops the host after RunAsync returns. + // Using named parameter 'setup' disambiguates from the 3-param overload. + var exception = Record.Exception(() => + BenchmarkProgram.Run(Array.Empty(), setup: (Action)null)); + + Assert.Null(exception); + TestOutput.WriteLine("Run completed without exception."); + } + + [Fact] + public void Run_WithFakeWorkspace_AndServiceConfigurator_ShouldInvokeConfigurator() + { + // Exercises Run(string[], Action, Action) + // and verifies that the serviceConfigurator delegate is invoked (line 102). + var configuratorWasCalled = false; + + var exception = Record.Exception(() => + BenchmarkProgram.Run( + Array.Empty(), + serviceConfigurator: services => + { + configuratorWasCalled = true; + services.AddSingleton(_ => null!); // harmless extra registration + })); + + Assert.Null(exception); + Assert.True(configuratorWasCalled, "serviceConfigurator should have been invoked during host setup."); + TestOutput.WriteLine("serviceConfigurator was invoked as expected."); + } + + [Fact] + public void Run_WithFakeWorkspace_AndNonEmptyArgs_ShouldUseBenchmarkSwitcherPath() + { + // Passing "--list flat" exercises the BenchmarkSwitcher branch in ExecuteBenchmarks. + // Using named parameter 'setup' disambiguates from the 3-param overload. + var exception = Record.Exception(() => + BenchmarkProgram.Run(new[] { "--list", "flat" }, setup: (Action)null)); + Assert.Null(exception); TestOutput.WriteLine("Run with '--list flat' completed (BenchmarkSwitcher path)."); } @@ -400,56 +400,56 @@ public async Task RunAsync_WithFakeWorkspace_AndNonEmptyArgs_ShouldUseBenchmarkS // --------------------------------------------------------------------------- // Helper: build a minimal service provider for RunAsync tests - // --------------------------------------------------------------------------- - - private static ServiceProvider BuildServiceProvider( - BenchmarkWorkspaceOptions options, - IBenchmarkWorkspace workspace, - BenchmarkContext context) - { - return new ServiceCollection() - .AddSingleton(workspace) - .AddSingleton(options) - .AddSingleton(context) - .BuildServiceProvider(); - } - - // --------------------------------------------------------------------------- - // Test doubles - // --------------------------------------------------------------------------- - - /// - /// A workspace that returns an empty assembly array and tracks whether - /// was called. - /// - private sealed class EmptyWorkspace : IBenchmarkWorkspace - { - public bool PostProcessArtifactsCalled { get; private set; } - - public Assembly[] LoadBenchmarkAssemblies() => Array.Empty(); - - public void PostProcessArtifacts() => PostProcessArtifactsCalled = true; - } - - /// - /// A workspace that returns a specific assembly so that benchmark-type discovery - /// (filtering by types whose names end with "Benchmark") finds . - /// - private sealed class AssemblyWorkspace : IBenchmarkWorkspace - { - private readonly Assembly _assembly; - - public AssemblyWorkspace(Assembly assembly) => _assembly = assembly; - - public Assembly[] LoadBenchmarkAssemblies() => new[] { _assembly }; - - public void PostProcessArtifacts() { } - } -} - -/// -/// A placeholder type whose name ends with "Benchmark" so that -/// 's filter logic can discover it via -/// t.Name.EndsWith("Benchmark"). -/// -internal sealed class KnownBenchmark { } + // --------------------------------------------------------------------------- + + private static ServiceProvider BuildServiceProvider( + BenchmarkWorkspaceOptions options, + IBenchmarkWorkspace workspace, + BenchmarkContext context) + { + return new ServiceCollection() + .AddSingleton(workspace) + .AddSingleton(options) + .AddSingleton(context) + .BuildServiceProvider(); + } + + // --------------------------------------------------------------------------- + // Test doubles + // --------------------------------------------------------------------------- + + /// + /// A workspace that returns an empty assembly array and tracks whether + /// was called. + /// + private sealed class EmptyWorkspace : IBenchmarkWorkspace + { + public bool PostProcessArtifactsCalled { get; private set; } + + public Assembly[] LoadBenchmarkAssemblies() => Array.Empty(); + + public void PostProcessArtifacts() => PostProcessArtifactsCalled = true; + } + + /// + /// A workspace that returns a specific assembly so that benchmark-type discovery + /// (filtering by types whose names end with "Benchmark") finds . + /// + private sealed class AssemblyWorkspace : IBenchmarkWorkspace + { + private readonly Assembly _assembly; + + public AssemblyWorkspace(Assembly assembly) => _assembly = assembly; + + public Assembly[] LoadBenchmarkAssemblies() => new[] { _assembly }; + + public void PostProcessArtifacts() { } + } +} + +/// +/// A placeholder type whose name ends with "Benchmark" so that +/// 's filter logic can discover it via +/// t.Name.EndsWith("Benchmark"). +/// +internal sealed class KnownBenchmark { } diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs index 5df38f3..148684c 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs @@ -1,223 +1,223 @@ -using Codebelt.Extensions.Xunit; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -public class BenchmarkContextTest : Test -{ - public BenchmarkContextTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void Constructor_ShouldInitializeWithEmptyArray_WhenArgsIsNull() - { - // Act - var context = new BenchmarkContext(null); - - // Assert - Assert.NotNull(context.Args); - Assert.Empty(context.Args); - } - - [Fact] - public void Constructor_ShouldInitializeWithEmptyArray_WhenArgsIsEmpty() - { - // Arrange - var emptyArgs = new string[] { }; - - // Act - var context = new BenchmarkContext(emptyArgs); - - // Assert - Assert.NotNull(context.Args); - Assert.Empty(context.Args); - Assert.Same(emptyArgs, context.Args); - } - - [Fact] - public void Constructor_ShouldInitializeWithProvidedArgs_WhenArgsIsNotEmpty() - { - // Arrange - var args = new[] { "arg1", "arg2", "arg3" }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(3, context.Args.Length); - Assert.Same(args, context.Args); - } - - [Fact] - public void Args_ShouldReturnOriginalArray_WhenProvided() - { - // Arrange - var args = new[] { "--filter", "MyBenchmark", "--job", "short" }; - var context = new BenchmarkContext(args); - - // Act - var result = context.Args; - - // Assert - Assert.Same(args, result); - Assert.Equal(4, result.Length); - Assert.Equal("--filter", result[0]); - Assert.Equal("MyBenchmark", result[1]); - Assert.Equal("--job", result[2]); - Assert.Equal("short", result[3]); - } - - [Fact] - public void Args_ShouldBeReadOnly_ButArrayContentsAreMutable() - { - // Arrange - var args = new[] { "original" }; - var context = new BenchmarkContext(args); - - // Act - Modify the original array - args[0] = "modified"; - - // Assert - The property reflects the change (no defensive copy) - Assert.Equal("modified", context.Args[0]); - } - - [Fact] - public void Constructor_ShouldHandleSingleArgument() - { - // Arrange - var args = new[] { "single-arg" }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Single(context.Args); - Assert.Equal("single-arg", context.Args[0]); - } - - [Fact] - public void Constructor_ShouldHandleArgsWithSpecialCharacters() - { - // Arrange - var args = new[] { "--config", "Debug", "--output", "C:\\Reports\\benchmark-results.txt" }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(4, context.Args.Length); - Assert.Equal("--config", context.Args[0]); - Assert.Equal("Debug", context.Args[1]); - Assert.Equal("--output", context.Args[2]); - Assert.Equal("C:\\Reports\\benchmark-results.txt", context.Args[3]); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(5)] - [InlineData(10)] - public void Constructor_ShouldHandleVariousArrayLengths(int length) - { - // Arrange - var args = new string[length]; - for (int i = 0; i < length; i++) - { - args[i] = $"arg{i}"; - } - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(length, context.Args.Length); - - TestOutput.WriteLine($"Created context with {length} arguments"); - } - - [Fact] - public void Constructor_ShouldHandleArgsWithEmptyStrings() - { - // Arrange - var args = new[] { "", "valid", "" }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(3, context.Args.Length); - Assert.Equal("", context.Args[0]); - Assert.Equal("valid", context.Args[1]); - Assert.Equal("", context.Args[2]); - } - - [Fact] - public void Constructor_ShouldHandleArgsWithWhitespace() - { - // Arrange - var args = new[] { " ", "valid", "\t\t" }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(3, context.Args.Length); - Assert.Equal(" ", context.Args[0]); - Assert.Equal("valid", context.Args[1]); - Assert.Equal("\t\t", context.Args[2]); - } - - [Fact] - public void Args_ShouldReturnEmptyArray_AfterConstructorWithNull() - { - // Arrange - var context = new BenchmarkContext(null); - - // Act - var args = context.Args; - - // Assert - Assert.NotNull(args); - Assert.Empty(args); - Assert.IsType(args); - } - - [Fact] - public void Constructor_ShouldHandleTypicalBenchmarkDotNetArgs() - { - // Arrange - var args = new[] - { - "--filter", - "Codebelt.Extensions.BenchmarkDotNet.*", - "--job", - "short", - "--exporters", - "markdown", - "--memory" - }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(7, context.Args.Length); - Assert.Equal("--filter", context.Args[0]); - Assert.Equal("Codebelt.Extensions.BenchmarkDotNet.*", context.Args[1]); - Assert.Equal("--job", context.Args[2]); - Assert.Equal("short", context.Args[3]); - Assert.Equal("--exporters", context.Args[4]); - Assert.Equal("markdown", context.Args[5]); - Assert.Equal("--memory", context.Args[6]); - - TestOutput.WriteLine($"Successfully created context with {context.Args.Length} BenchmarkDotNet arguments"); - } -} +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +public class BenchmarkContextTest : Test +{ + public BenchmarkContextTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldInitializeWithEmptyArray_WhenArgsIsNull() + { + // Act + var context = new BenchmarkContext(null); + + // Assert + Assert.NotNull(context.Args); + Assert.Empty(context.Args); + } + + [Fact] + public void Constructor_ShouldInitializeWithEmptyArray_WhenArgsIsEmpty() + { + // Arrange + var emptyArgs = new string[] { }; + + // Act + var context = new BenchmarkContext(emptyArgs); + + // Assert + Assert.NotNull(context.Args); + Assert.Empty(context.Args); + Assert.Same(emptyArgs, context.Args); + } + + [Fact] + public void Constructor_ShouldInitializeWithProvidedArgs_WhenArgsIsNotEmpty() + { + // Arrange + var args = new[] { "arg1", "arg2", "arg3" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(3, context.Args.Length); + Assert.Same(args, context.Args); + } + + [Fact] + public void Args_ShouldReturnOriginalArray_WhenProvided() + { + // Arrange + var args = new[] { "--filter", "MyBenchmark", "--job", "short" }; + var context = new BenchmarkContext(args); + + // Act + var result = context.Args; + + // Assert + Assert.Same(args, result); + Assert.Equal(4, result.Length); + Assert.Equal("--filter", result[0]); + Assert.Equal("MyBenchmark", result[1]); + Assert.Equal("--job", result[2]); + Assert.Equal("short", result[3]); + } + + [Fact] + public void Args_ShouldBeReadOnly_ButArrayContentsAreMutable() + { + // Arrange + var args = new[] { "original" }; + var context = new BenchmarkContext(args); + + // Act - Modify the original array + args[0] = "modified"; + + // Assert - The property reflects the change (no defensive copy) + Assert.Equal("modified", context.Args[0]); + } + + [Fact] + public void Constructor_ShouldHandleSingleArgument() + { + // Arrange + var args = new[] { "single-arg" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Single(context.Args); + Assert.Equal("single-arg", context.Args[0]); + } + + [Fact] + public void Constructor_ShouldHandleArgsWithSpecialCharacters() + { + // Arrange + var args = new[] { "--config", "Debug", "--output", "C:\\Reports\\benchmark-results.txt" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(4, context.Args.Length); + Assert.Equal("--config", context.Args[0]); + Assert.Equal("Debug", context.Args[1]); + Assert.Equal("--output", context.Args[2]); + Assert.Equal("C:\\Reports\\benchmark-results.txt", context.Args[3]); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + [InlineData(10)] + public void Constructor_ShouldHandleVariousArrayLengths(int length) + { + // Arrange + var args = new string[length]; + for (int i = 0; i < length; i++) + { + args[i] = $"arg{i}"; + } + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(length, context.Args.Length); + + TestOutput.WriteLine($"Created context with {length} arguments"); + } + + [Fact] + public void Constructor_ShouldHandleArgsWithEmptyStrings() + { + // Arrange + var args = new[] { "", "valid", "" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(3, context.Args.Length); + Assert.Equal("", context.Args[0]); + Assert.Equal("valid", context.Args[1]); + Assert.Equal("", context.Args[2]); + } + + [Fact] + public void Constructor_ShouldHandleArgsWithWhitespace() + { + // Arrange + var args = new[] { " ", "valid", "\t\t" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(3, context.Args.Length); + Assert.Equal(" ", context.Args[0]); + Assert.Equal("valid", context.Args[1]); + Assert.Equal("\t\t", context.Args[2]); + } + + [Fact] + public void Args_ShouldReturnEmptyArray_AfterConstructorWithNull() + { + // Arrange + var context = new BenchmarkContext(null); + + // Act + var args = context.Args; + + // Assert + Assert.NotNull(args); + Assert.Empty(args); + Assert.IsType(args); + } + + [Fact] + public void Constructor_ShouldHandleTypicalBenchmarkDotNetArgs() + { + // Arrange + var args = new[] + { + "--filter", + "Codebelt.Extensions.BenchmarkDotNet.*", + "--job", + "short", + "--exporters", + "markdown", + "--memory" + }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(7, context.Args.Length); + Assert.Equal("--filter", context.Args[0]); + Assert.Equal("Codebelt.Extensions.BenchmarkDotNet.*", context.Args[1]); + Assert.Equal("--job", context.Args[2]); + Assert.Equal("short", context.Args[3]); + Assert.Equal("--exporters", context.Args[4]); + Assert.Equal("markdown", context.Args[5]); + Assert.Equal("--memory", context.Args[6]); + + TestOutput.WriteLine($"Successfully created context with {context.Args.Length} BenchmarkDotNet arguments"); + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs index 67a8964..3291f51 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs @@ -4,349 +4,349 @@ using System.Linq; using System.Threading.Tasks; using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -public class BenchmarkProgramTest : Test -{ - public BenchmarkProgramTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void BuildConfiguration_ShouldReturnValidValue() - { - // Act - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - - // Assert - Assert.NotNull(buildConfiguration); - Assert.True(buildConfiguration == "Debug" || buildConfiguration == "Release"); - - TestOutput.WriteLine($"BuildConfiguration: {buildConfiguration}"); - } - - [Fact] - public void IsDebugBuild_ShouldBeConsistentWithBuildConfiguration() - { - // Act - var isDebugBuild = BenchmarkProgram.IsDebugBuild; - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - - // Assert - if (buildConfiguration == "Debug") - { - Assert.True(isDebugBuild); - } - else if (buildConfiguration == "Release") - { - Assert.False(isDebugBuild); - } - - TestOutput.WriteLine($"IsDebugBuild: {isDebugBuild}, BuildConfiguration: {buildConfiguration}"); - } - - [Fact] - public void BuildConfiguration_ShouldBeDebug_WhenCompiledInDebugMode() - { - // Arrange - var entryAssembly = Assembly.GetEntryAssembly(); - if (entryAssembly == null) - { - // Skip test if entry assembly is not available (e.g., when running in certain test environments) - TestOutput.WriteLine("Entry assembly is null, skipping test"); - return; - } - - if (entryAssembly.FullName.Contains("ReSharper", StringComparison.OrdinalIgnoreCase)) - { - // Skip test if running under ReSharper - TestOutput.WriteLine("Running under ReSharper, skipping test"); - return; - } - - // Act - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - - // Assert - #if DEBUG - Assert.Equal("Debug", buildConfiguration); - #else - Assert.Equal("Release", buildConfiguration); - #endif - - TestOutput.WriteLine($"BuildConfiguration: {buildConfiguration}"); - } - - [Fact] - public void IsDebugBuild_ShouldBeTrue_WhenCompiledInDebugMode() - { - // Arrange - var entryAssembly = Assembly.GetEntryAssembly(); - if (entryAssembly == null) - { - // Skip test if entry assembly is not available - TestOutput.WriteLine("Entry assembly is null, skipping test"); - return; - } - - if (entryAssembly.FullName.Contains("ReSharper", StringComparison.OrdinalIgnoreCase)) - { - // Skip test if running under ReSharper - TestOutput.WriteLine("Running under ReSharper, skipping test"); - return; - } - - // Act - var isDebugBuild = BenchmarkProgram.IsDebugBuild; - - // Assert - #if DEBUG - Assert.True(isDebugBuild); - #else - Assert.False(isDebugBuild); - #endif - - TestOutput.WriteLine($"IsDebugBuild: {isDebugBuild}"); - } - - [Fact] - public void BuildConfiguration_ShouldBeCached_AcrossMultipleCalls() - { - // Act - var firstCall = BenchmarkProgram.BuildConfiguration; - var secondCall = BenchmarkProgram.BuildConfiguration; - var thirdCall = BenchmarkProgram.BuildConfiguration; - - // Assert - Assert.Equal(firstCall, secondCall); - Assert.Equal(secondCall, thirdCall); - Assert.Same(firstCall, secondCall); // Should be the same string instance - - TestOutput.WriteLine($"BuildConfiguration called 3 times, all returned: {firstCall}"); - } - - [Fact] - public void IsDebugBuild_ShouldBeCached_AcrossMultipleCalls() - { - // Act - var firstCall = BenchmarkProgram.IsDebugBuild; - var secondCall = BenchmarkProgram.IsDebugBuild; - var thirdCall = BenchmarkProgram.IsDebugBuild; - - // Assert - Assert.Equal(firstCall, secondCall); - Assert.Equal(secondCall, thirdCall); - - TestOutput.WriteLine($"IsDebugBuild called 3 times, all returned: {firstCall}"); - } - - [Fact] - public void StaticProperties_ShouldBeInitializedAtStartup() - { - // Act & Assert - Static constructor should have already run - Assert.NotNull(BenchmarkProgram.BuildConfiguration); - Assert.NotEmpty(BenchmarkProgram.BuildConfiguration); - - // IsDebugBuild can be true or false, but should be set - var isDebugBuild = BenchmarkProgram.IsDebugBuild; - Assert.True(isDebugBuild == true || isDebugBuild == false); - - TestOutput.WriteLine($"BuildConfiguration: {BenchmarkProgram.BuildConfiguration}"); - TestOutput.WriteLine($"IsDebugBuild: {BenchmarkProgram.IsDebugBuild}"); - } - - [Theory] - [InlineData("Debug", true)] - [InlineData("Release", false)] - public void BuildConfiguration_ShouldMatchExpectedDebugState(string expectedConfig, bool expectedIsDebug) - { - // Act - var actualConfig = BenchmarkProgram.BuildConfiguration; - var actualIsDebug = BenchmarkProgram.IsDebugBuild; - - // Assert - if (actualConfig == expectedConfig) - { - Assert.Equal(expectedIsDebug, actualIsDebug); - TestOutput.WriteLine($"Configuration '{actualConfig}' correctly maps to IsDebugBuild={actualIsDebug}"); - } - else - { - TestOutput.WriteLine($"Skipping assertion - actual configuration is '{actualConfig}', not '{expectedConfig}'"); - } - } - - [Fact] - public void BuildConfiguration_ShouldNotBeNullOrEmpty() - { - // Act - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - - // Assert - Assert.NotNull(buildConfiguration); - Assert.NotEmpty(buildConfiguration); - Assert.False(string.IsNullOrWhiteSpace(buildConfiguration)); - - TestOutput.WriteLine($"BuildConfiguration: '{buildConfiguration}'"); - } - - [Fact] - public void BuildConfiguration_ShouldBeOneOfTwoValidValues() - { - // Act - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - - // Assert - Assert.Contains(buildConfiguration, new[] { "Debug", "Release" }); - - TestOutput.WriteLine($"BuildConfiguration is valid: {buildConfiguration}"); - } - - [Fact] - public void IsDebugBuild_ShouldBeBoolean() - { - // Act - var isDebugBuild = BenchmarkProgram.IsDebugBuild; - - // Assert - Assert.IsType(isDebugBuild); - - TestOutput.WriteLine($"IsDebugBuild type verified: {isDebugBuild}"); - } - - [Fact] - public void StaticConstructor_ShouldSetPropertiesBasedOnEntryAssembly() - { - // This test verifies that the static constructor logic has executed correctly - // by checking that both properties are set and consistent with each other - - // Act - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - var isDebugBuild = BenchmarkProgram.IsDebugBuild; - - // Assert - Assert.NotNull(buildConfiguration); - - if (isDebugBuild) - { - Assert.Equal("Debug", buildConfiguration); - } - else - { - Assert.Equal("Release", buildConfiguration); - } - - TestOutput.WriteLine($"Static properties correctly initialized - BuildConfiguration: {buildConfiguration}, IsDebugBuild: {isDebugBuild}"); - } - - [Fact] - public void BenchmarkProgram_ShouldInheritFromConsoleProgram() - { - // Act - var baseType = typeof(BenchmarkProgram).BaseType; - - // Assert - Assert.NotNull(baseType); - Assert.True(baseType.IsGenericType); - Assert.Equal("MinimalConsoleProgram`1", baseType.Name); - - TestOutput.WriteLine($"BenchmarkProgram correctly inherits from: {baseType.FullName}"); - } - - [Fact] - public void BenchmarkProgram_ShouldUseCorrectGenericTypeParameter() - { - // Act - var baseType = typeof(BenchmarkProgram).BaseType; - var genericArguments = baseType?.GetGenericArguments(); - - // Assert - Assert.NotNull(genericArguments); - Assert.Single(genericArguments); - Assert.Equal(typeof(BenchmarkProgram), genericArguments[0]); - - TestOutput.WriteLine($"BenchmarkProgram uses correct generic type parameter: {genericArguments[0].Name}"); - } - - [Fact] - public void BenchmarkProgram_ShouldBePublicClass() - { - // Act - var type = typeof(BenchmarkProgram); - - // Assert - Assert.True(type.IsPublic); - Assert.True(type.IsClass); - Assert.False(type.IsAbstract); - Assert.False(type.IsSealed); - - TestOutput.WriteLine($"BenchmarkProgram is a public, non-abstract, non-sealed class"); - } - - [Fact] - public void Run_MethodWithDefaultWorkspace_ShouldExist() - { - // Act - var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => - m.Name == "Run" && - !m.IsGenericMethodDefinition && - m.GetParameters().Length == 2 && - m.GetParameters()[0].ParameterType == typeof(string[]) && - m.GetParameters()[1].ParameterType == typeof(Action)); - - // Assert - Assert.NotNull(method); - Assert.True(method.IsStatic); - Assert.True(method.IsPublic); - Assert.Equal(typeof(void), method.ReturnType); - - TestOutput.WriteLine($"Found Run method with default workspace: {method}"); - } - - [Fact] - public void Run_GenericMethod_ShouldExist() - { - // Act - var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => - m.Name == "Run" && - m.IsGenericMethodDefinition && - m.GetParameters().Length == 2); - - // Assert - Assert.NotNull(method); - Assert.True(method.IsStatic); - Assert.True(method.IsPublic); - Assert.True(method.IsGenericMethodDefinition); - Assert.Equal(typeof(void), method.ReturnType); - - var genericArguments = method.GetGenericArguments(); - Assert.Single(genericArguments); - - TestOutput.WriteLine($"Found generic Run method: {method}"); - } - - [Fact] + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +public class BenchmarkProgramTest : Test +{ + public BenchmarkProgramTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void BuildConfiguration_ShouldReturnValidValue() + { + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.NotNull(buildConfiguration); + Assert.True(buildConfiguration == "Debug" || buildConfiguration == "Release"); + + TestOutput.WriteLine($"BuildConfiguration: {buildConfiguration}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeConsistentWithBuildConfiguration() + { + // Act + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + if (buildConfiguration == "Debug") + { + Assert.True(isDebugBuild); + } + else if (buildConfiguration == "Release") + { + Assert.False(isDebugBuild); + } + + TestOutput.WriteLine($"IsDebugBuild: {isDebugBuild}, BuildConfiguration: {buildConfiguration}"); + } + + [Fact] + public void BuildConfiguration_ShouldBeDebug_WhenCompiledInDebugMode() + { + // Arrange + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly == null) + { + // Skip test if entry assembly is not available (e.g., when running in certain test environments) + TestOutput.WriteLine("Entry assembly is null, skipping test"); + return; + } + + if (entryAssembly.FullName.Contains("ReSharper", StringComparison.OrdinalIgnoreCase)) + { + // Skip test if running under ReSharper + TestOutput.WriteLine("Running under ReSharper, skipping test"); + return; + } + + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert +#if DEBUG + Assert.Equal("Debug", buildConfiguration); +#else + Assert.Equal("Release", buildConfiguration); +#endif + + TestOutput.WriteLine($"BuildConfiguration: {buildConfiguration}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeTrue_WhenCompiledInDebugMode() + { + // Arrange + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly == null) + { + // Skip test if entry assembly is not available + TestOutput.WriteLine("Entry assembly is null, skipping test"); + return; + } + + if (entryAssembly.FullName.Contains("ReSharper", StringComparison.OrdinalIgnoreCase)) + { + // Skip test if running under ReSharper + TestOutput.WriteLine("Running under ReSharper, skipping test"); + return; + } + + // Act + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + + // Assert +#if DEBUG + Assert.True(isDebugBuild); +#else + Assert.False(isDebugBuild); +#endif + + TestOutput.WriteLine($"IsDebugBuild: {isDebugBuild}"); + } + + [Fact] + public void BuildConfiguration_ShouldBeCached_AcrossMultipleCalls() + { + // Act + var firstCall = BenchmarkProgram.BuildConfiguration; + var secondCall = BenchmarkProgram.BuildConfiguration; + var thirdCall = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.Equal(firstCall, secondCall); + Assert.Equal(secondCall, thirdCall); + Assert.Same(firstCall, secondCall); // Should be the same string instance + + TestOutput.WriteLine($"BuildConfiguration called 3 times, all returned: {firstCall}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeCached_AcrossMultipleCalls() + { + // Act + var firstCall = BenchmarkProgram.IsDebugBuild; + var secondCall = BenchmarkProgram.IsDebugBuild; + var thirdCall = BenchmarkProgram.IsDebugBuild; + + // Assert + Assert.Equal(firstCall, secondCall); + Assert.Equal(secondCall, thirdCall); + + TestOutput.WriteLine($"IsDebugBuild called 3 times, all returned: {firstCall}"); + } + + [Fact] + public void StaticProperties_ShouldBeInitializedAtStartup() + { + // Act & Assert - Static constructor should have already run + Assert.NotNull(BenchmarkProgram.BuildConfiguration); + Assert.NotEmpty(BenchmarkProgram.BuildConfiguration); + + // IsDebugBuild can be true or false, but should be set + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + Assert.True(isDebugBuild == true || isDebugBuild == false); + + TestOutput.WriteLine($"BuildConfiguration: {BenchmarkProgram.BuildConfiguration}"); + TestOutput.WriteLine($"IsDebugBuild: {BenchmarkProgram.IsDebugBuild}"); + } + + [Theory] + [InlineData("Debug", true)] + [InlineData("Release", false)] + public void BuildConfiguration_ShouldMatchExpectedDebugState(string expectedConfig, bool expectedIsDebug) + { + // Act + var actualConfig = BenchmarkProgram.BuildConfiguration; + var actualIsDebug = BenchmarkProgram.IsDebugBuild; + + // Assert + if (actualConfig == expectedConfig) + { + Assert.Equal(expectedIsDebug, actualIsDebug); + TestOutput.WriteLine($"Configuration '{actualConfig}' correctly maps to IsDebugBuild={actualIsDebug}"); + } + else + { + TestOutput.WriteLine($"Skipping assertion - actual configuration is '{actualConfig}', not '{expectedConfig}'"); + } + } + + [Fact] + public void BuildConfiguration_ShouldNotBeNullOrEmpty() + { + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.NotNull(buildConfiguration); + Assert.NotEmpty(buildConfiguration); + Assert.False(string.IsNullOrWhiteSpace(buildConfiguration)); + + TestOutput.WriteLine($"BuildConfiguration: '{buildConfiguration}'"); + } + + [Fact] + public void BuildConfiguration_ShouldBeOneOfTwoValidValues() + { + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.Contains(buildConfiguration, new[] { "Debug", "Release" }); + + TestOutput.WriteLine($"BuildConfiguration is valid: {buildConfiguration}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeBoolean() + { + // Act + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + + // Assert + Assert.IsType(isDebugBuild); + + TestOutput.WriteLine($"IsDebugBuild type verified: {isDebugBuild}"); + } + + [Fact] + public void StaticConstructor_ShouldSetPropertiesBasedOnEntryAssembly() + { + // This test verifies that the static constructor logic has executed correctly + // by checking that both properties are set and consistent with each other + + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + + // Assert + Assert.NotNull(buildConfiguration); + + if (isDebugBuild) + { + Assert.Equal("Debug", buildConfiguration); + } + else + { + Assert.Equal("Release", buildConfiguration); + } + + TestOutput.WriteLine($"Static properties correctly initialized - BuildConfiguration: {buildConfiguration}, IsDebugBuild: {isDebugBuild}"); + } + + [Fact] + public void BenchmarkProgram_ShouldInheritFromConsoleProgram() + { + // Act + var baseType = typeof(BenchmarkProgram).BaseType; + + // Assert + Assert.NotNull(baseType); + Assert.True(baseType.IsGenericType); + Assert.Equal("MinimalConsoleProgram`1", baseType.Name); + + TestOutput.WriteLine($"BenchmarkProgram correctly inherits from: {baseType.FullName}"); + } + + [Fact] + public void BenchmarkProgram_ShouldUseCorrectGenericTypeParameter() + { + // Act + var baseType = typeof(BenchmarkProgram).BaseType; + var genericArguments = baseType?.GetGenericArguments(); + + // Assert + Assert.NotNull(genericArguments); + Assert.Single(genericArguments); + Assert.Equal(typeof(BenchmarkProgram), genericArguments[0]); + + TestOutput.WriteLine($"BenchmarkProgram uses correct generic type parameter: {genericArguments[0].Name}"); + } + + [Fact] + public void BenchmarkProgram_ShouldBePublicClass() + { + // Act + var type = typeof(BenchmarkProgram); + + // Assert + Assert.True(type.IsPublic); + Assert.True(type.IsClass); + Assert.False(type.IsAbstract); + Assert.False(type.IsSealed); + + TestOutput.WriteLine($"BenchmarkProgram is a public, non-abstract, non-sealed class"); + } + + [Fact] + public void Run_MethodWithDefaultWorkspace_ShouldExist() + { + // Act + var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + !m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[0].ParameterType == typeof(string[]) && + m.GetParameters()[1].ParameterType == typeof(Action)); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsStatic); + Assert.True(method.IsPublic); + Assert.Equal(typeof(void), method.ReturnType); + + TestOutput.WriteLine($"Found Run method with default workspace: {method}"); + } + + [Fact] + public void Run_GenericMethod_ShouldExist() + { + // Act + var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + m.IsGenericMethodDefinition && + m.GetParameters().Length == 2); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsStatic); + Assert.True(method.IsPublic); + Assert.True(method.IsGenericMethodDefinition); + Assert.Equal(typeof(void), method.ReturnType); + + var genericArguments = method.GetGenericArguments(); + Assert.Single(genericArguments); + + TestOutput.WriteLine($"Found generic Run method: {method}"); + } + + [Fact] public void Run_GenericMethod_ShouldHaveCorrectConstraints() { // Act var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => - m.Name == "Run" && - m.IsGenericMethodDefinition && - m.GetParameters().Length == 2); - - Assert.NotNull(method); - - var genericArguments = method.GetGenericArguments(); - var typeParameter = genericArguments[0]; - - // Assert - var constraints = typeParameter.GetGenericParameterConstraints(); - Assert.Contains(constraints, c => c == typeof(IBenchmarkWorkspace)); - Assert.True(typeParameter.GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)); + .FirstOrDefault(m => + m.Name == "Run" && + m.IsGenericMethodDefinition && + m.GetParameters().Length == 2); + + Assert.NotNull(method); + + var genericArguments = method.GetGenericArguments(); + var typeParameter = genericArguments[0]; + + // Assert + var constraints = typeParameter.GetGenericParameterConstraints(); + Assert.Contains(constraints, c => c == typeof(IBenchmarkWorkspace)); + Assert.True(typeParameter.GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)); TestOutput.WriteLine($"Generic type parameter '{typeParameter.Name}' has correct constraints: class, IBenchmarkWorkspace"); } @@ -421,63 +421,63 @@ public void RunAsync_GenericMethod_ShouldHaveCorrectConstraints() [Fact] public void StaticProperties_ShouldBeReadOnly() { - // Act - var buildConfigProperty = typeof(BenchmarkProgram).GetProperty("BuildConfiguration"); - var isDebugBuildProperty = typeof(BenchmarkProgram).GetProperty("IsDebugBuild"); - - // Assert - Assert.NotNull(buildConfigProperty); - Assert.True(buildConfigProperty.CanRead); - Assert.False(buildConfigProperty.CanWrite); - - Assert.NotNull(isDebugBuildProperty); - Assert.True(isDebugBuildProperty.CanRead); - Assert.False(isDebugBuildProperty.CanWrite); - - TestOutput.WriteLine("Both static properties are read-only as expected"); - } - - [Fact] - public void Run_Methods_ShouldHaveCorrectParameterNames() - { - // Act - Get the non-generic Run method explicitly - var nonGenericMethod = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => - m.Name == "Run" && - !m.IsGenericMethodDefinition && - m.GetParameters().Length == 2 && - m.GetParameters()[0].ParameterType == typeof(string[]) && - m.GetParameters()[1].ParameterType == typeof(Action)); - - // Assert - Assert.NotNull(nonGenericMethod); - - var parameters = nonGenericMethod.GetParameters(); - Assert.Equal(2, parameters.Length); - Assert.Equal("args", parameters[0].Name); - Assert.Equal("setup", parameters[1].Name); - - TestOutput.WriteLine($"Run method parameters: {string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"))}"); - } - - [Fact] + // Act + var buildConfigProperty = typeof(BenchmarkProgram).GetProperty("BuildConfiguration"); + var isDebugBuildProperty = typeof(BenchmarkProgram).GetProperty("IsDebugBuild"); + + // Assert + Assert.NotNull(buildConfigProperty); + Assert.True(buildConfigProperty.CanRead); + Assert.False(buildConfigProperty.CanWrite); + + Assert.NotNull(isDebugBuildProperty); + Assert.True(isDebugBuildProperty.CanRead); + Assert.False(isDebugBuildProperty.CanWrite); + + TestOutput.WriteLine("Both static properties are read-only as expected"); + } + + [Fact] + public void Run_Methods_ShouldHaveCorrectParameterNames() + { + // Act - Get the non-generic Run method explicitly + var nonGenericMethod = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + !m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[0].ParameterType == typeof(string[]) && + m.GetParameters()[1].ParameterType == typeof(Action)); + + // Assert + Assert.NotNull(nonGenericMethod); + + var parameters = nonGenericMethod.GetParameters(); + Assert.Equal(2, parameters.Length); + Assert.Equal("args", parameters[0].Name); + Assert.Equal("setup", parameters[1].Name); + + TestOutput.WriteLine($"Run method parameters: {string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"))}"); + } + + [Fact] public void Run_Methods_ShouldHaveOptionalSetupParameter() { // Act - Get the non-generic Run method explicitly var nonGenericMethod = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => - m.Name == "Run" && - !m.IsGenericMethodDefinition && - m.GetParameters().Length == 2 && - m.GetParameters()[0].ParameterType == typeof(string[]) && - m.GetParameters()[1].ParameterType == typeof(Action)); - - // Assert - Assert.NotNull(nonGenericMethod); - - var setupParameter = nonGenericMethod.GetParameters()[1]; - Assert.True(setupParameter.IsOptional); - Assert.Null(setupParameter.DefaultValue); + .FirstOrDefault(m => + m.Name == "Run" && + !m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[0].ParameterType == typeof(string[]) && + m.GetParameters()[1].ParameterType == typeof(Action)); + + // Assert + Assert.NotNull(nonGenericMethod); + + var setupParameter = nonGenericMethod.GetParameters()[1]; + Assert.True(setupParameter.IsOptional); + Assert.Null(setupParameter.DefaultValue); TestOutput.WriteLine($"Setup parameter is optional with default value: {setupParameter.DefaultValue ?? "null"}"); } diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkProgramTest.cs index 19128e7..42917e9 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkProgramTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkProgramTest.cs @@ -1,145 +1,145 @@ -using BenchmarkDotNet.Configs; -using Codebelt.Extensions.BenchmarkDotNet.Console; -using Codebelt.Extensions.Xunit; -using System; -using System.IO; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -/// -/// Functional tests for non-generic overloads and end-to-end host lifecycle -/// using the real against the existing tuning assemblies. -/// -public class BenchmarkProgramTest : Test -{ - // BenchmarkProgram.IsDebugBuild reflects whether the entry assembly was compiled in Debug mode, - // which is what BenchmarkWorkspace uses to locate tuning assemblies under bin/Debug or bin/Release. - private static readonly bool IsDebugBuild = BenchmarkProgram.IsDebugBuild; - - public BenchmarkProgramTest(ITestOutputHelper output) : base(output) - { - } - - /// - /// Exercises the non-generic Run(string[], Action<BenchmarkWorkspaceOptions>) overload (line 54) - /// and the non-generic Run(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>) - /// overload (line 68) which both ultimately delegate to - /// Run<BenchmarkWorkspace>(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>). - /// - /// Using "--list flat" makes BenchmarkSwitcher enumerate available benchmarks and return immediately - /// without executing any, which keeps the test fast and deterministic. - /// - [Fact] - public void Run_NonGeneric_WithListFlatArg_ShouldCompleteWithoutException() - { - var exception = Record.Exception(() => - BenchmarkProgram.Run( - new[] { "--list", "flat" }, - options => - { - options.AllowDebugBuild = IsDebugBuild; - options.Configuration = ManualConfig.CreateEmpty() - .WithOptions(ConfigOptions.DisableLogFile | ConfigOptions.DisableOptimizationsValidator); - })); - - Assert.Null(exception); - TestOutput.WriteLine("Non-generic BenchmarkProgram.Run completed via '--list flat'."); - } - - /// - /// Exercises the non-generic Run(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>) - /// overload with an explicit to ensure the three-argument - /// non-generic overload (line 68) is reached. - /// - [Fact] - public void Run_NonGenericWithServiceConfigurator_WithListFlatArg_ShouldCompleteWithoutException() - { - var configuratorCalled = false; - - var exception = Record.Exception(() => - BenchmarkProgram.Run( - new[] { "--list", "flat" }, - services => { configuratorCalled = true; }, - options => - { - options.AllowDebugBuild = IsDebugBuild; - options.Configuration = ManualConfig.CreateEmpty() - .WithOptions(ConfigOptions.DisableLogFile | ConfigOptions.DisableOptimizationsValidator); - })); - - Assert.Null(exception); - Assert.True(configuratorCalled); - TestOutput.WriteLine("Non-generic BenchmarkProgram.Run with service configurator completed."); - } - - /// - /// Verifies that loads the tuning assemblies - /// built for the current configuration and TFM, and that the assembly resolver hook is registered. - /// Running this test with the real workspace may cause the - /// event to fire for transitive dependencies discovered only through the tuning-folder DLL scan. - /// - [Fact] - public void BenchmarkWorkspace_LoadBenchmarkAssemblies_ShouldLoadTuningAssemblies() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.NotEmpty(assemblies); - - foreach (var assembly in assemblies) - { - TestOutput.WriteLine($"Loaded: {assembly.GetName().Name}"); - } - } - - /// - /// Verifies that behaves correctly when the - /// results directory already has files and then calls delete. This exercises the full cleanup path. - /// - [Fact] - public void BenchmarkWorkspace_PostProcessArtifacts_ShouldMoveFilesAndDeleteResultsDir() - { - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - Directory.CreateDirectory(resultsDir); - - File.WriteAllText(Path.Combine(resultsDir, "SomeBenchmark-report.md"), "# report"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - workspace.PostProcessArtifacts(); - - // Assert - Assert.False(Directory.Exists(resultsDir)); - Assert.True(File.Exists(Path.Combine(tuningDir, "SomeBenchmark-report.md"))); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } -} +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.BenchmarkDotNet.Console; +using Codebelt.Extensions.Xunit; +using System; +using System.IO; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Functional tests for non-generic overloads and end-to-end host lifecycle +/// using the real against the existing tuning assemblies. +/// +public class BenchmarkProgramTest : Test +{ + // BenchmarkProgram.IsDebugBuild reflects whether the entry assembly was compiled in Debug mode, + // which is what BenchmarkWorkspace uses to locate tuning assemblies under bin/Debug or bin/Release. + private static readonly bool IsDebugBuild = BenchmarkProgram.IsDebugBuild; + + public BenchmarkProgramTest(ITestOutputHelper output) : base(output) + { + } + + /// + /// Exercises the non-generic Run(string[], Action<BenchmarkWorkspaceOptions>) overload (line 54) + /// and the non-generic Run(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>) + /// overload (line 68) which both ultimately delegate to + /// Run<BenchmarkWorkspace>(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>). + /// + /// Using "--list flat" makes BenchmarkSwitcher enumerate available benchmarks and return immediately + /// without executing any, which keeps the test fast and deterministic. + /// + [Fact] + public void Run_NonGeneric_WithListFlatArg_ShouldCompleteWithoutException() + { + var exception = Record.Exception(() => + BenchmarkProgram.Run( + new[] { "--list", "flat" }, + options => + { + options.AllowDebugBuild = IsDebugBuild; + options.Configuration = ManualConfig.CreateEmpty() + .WithOptions(ConfigOptions.DisableLogFile | ConfigOptions.DisableOptimizationsValidator); + })); + + Assert.Null(exception); + TestOutput.WriteLine("Non-generic BenchmarkProgram.Run completed via '--list flat'."); + } + + /// + /// Exercises the non-generic Run(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>) + /// overload with an explicit to ensure the three-argument + /// non-generic overload (line 68) is reached. + /// + [Fact] + public void Run_NonGenericWithServiceConfigurator_WithListFlatArg_ShouldCompleteWithoutException() + { + var configuratorCalled = false; + + var exception = Record.Exception(() => + BenchmarkProgram.Run( + new[] { "--list", "flat" }, + services => { configuratorCalled = true; }, + options => + { + options.AllowDebugBuild = IsDebugBuild; + options.Configuration = ManualConfig.CreateEmpty() + .WithOptions(ConfigOptions.DisableLogFile | ConfigOptions.DisableOptimizationsValidator); + })); + + Assert.Null(exception); + Assert.True(configuratorCalled); + TestOutput.WriteLine("Non-generic BenchmarkProgram.Run with service configurator completed."); + } + + /// + /// Verifies that loads the tuning assemblies + /// built for the current configuration and TFM, and that the assembly resolver hook is registered. + /// Running this test with the real workspace may cause the + /// event to fire for transitive dependencies discovered only through the tuning-folder DLL scan. + /// + [Fact] + public void BenchmarkWorkspace_LoadBenchmarkAssemblies_ShouldLoadTuningAssemblies() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.NotEmpty(assemblies); + + foreach (var assembly in assemblies) + { + TestOutput.WriteLine($"Loaded: {assembly.GetName().Name}"); + } + } + + /// + /// Verifies that behaves correctly when the + /// results directory already has files and then calls delete. This exercises the full cleanup path. + /// + [Fact] + public void BenchmarkWorkspace_PostProcessArtifacts_ShouldMoveFilesAndDeleteResultsDir() + { + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + Directory.CreateDirectory(resultsDir); + + File.WriteAllText(Path.Combine(resultsDir, "SomeBenchmark-report.md"), "# report"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.True(File.Exists(Path.Combine(tuningDir, "SomeBenchmark-report.md"))); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs index f0d7697..6254002 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs @@ -1,754 +1,754 @@ -using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Configs; using Codebelt.Extensions.Xunit; using System; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -public class BenchmarkWorkspaceOptionsTest : Test -{ - public BenchmarkWorkspaceOptionsTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void Constructor_ShouldInitializeWithDefaultValues() - { - // Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.RepositoryPath); - Assert.NotNull(options.Configuration); - Assert.NotNull(options.TargetFrameworkMoniker); - Assert.Equal(BenchmarkWorkspaceOptions.DefaultRepositoryTuningFolder, options.RepositoryTuningFolder); - Assert.Equal(BenchmarkWorkspaceOptions.DefaultRepositoryReportsFolder, options.RepositoryReportsFolder); - Assert.Equal(BenchmarkWorkspaceOptions.DefaultBenchmarkProjectSuffix, options.BenchmarkProjectSuffix); - Assert.False(options.AllowDebugBuild); - } - - [Fact] - public void DefaultRepositoryReportsFolder_ShouldBeReports() - { - // Assert - Assert.Equal("reports", BenchmarkWorkspaceOptions.DefaultRepositoryReportsFolder); - } - - [Fact] - public void DefaultRepositoryTuningFolder_ShouldBeTuning() - { - // Assert - Assert.Equal("tuning", BenchmarkWorkspaceOptions.DefaultRepositoryTuningFolder); - } - - [Fact] - public void DefaultBenchmarkProjectSuffix_ShouldBeBenchmarks() - { - // Assert - Assert.Equal("Benchmarks", BenchmarkWorkspaceOptions.DefaultBenchmarkProjectSuffix); - } - - [Fact] - public void RepositoryPath_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testPath = @"C:\TestRepo"; - - // Act - options.RepositoryPath = testPath; - - // Assert - Assert.Equal(testPath, options.RepositoryPath); - } - - [Fact] - public void Configuration_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var customConfig = ManualConfig.CreateEmpty(); - - // Act - options.Configuration = customConfig; - - // Assert - Assert.Same(customConfig, options.Configuration); - } - - [Fact] - public void TargetFrameworkMoniker_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testTfm = "net8.0"; - - // Act - options.TargetFrameworkMoniker = testTfm; - - // Assert - Assert.Equal(testTfm, options.TargetFrameworkMoniker); - } - - [Fact] - public void RepositoryTuningFolder_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testFolder = "custom-tuning"; - - // Act - options.RepositoryTuningFolder = testFolder; - - // Assert - Assert.Equal(testFolder, options.RepositoryTuningFolder); - } - - [Fact] - public void RepositoryReportsFolder_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testFolder = "custom-reports"; - - // Act - options.RepositoryReportsFolder = testFolder; - - // Assert - Assert.Equal(testFolder, options.RepositoryReportsFolder); - } - - [Fact] - public void BenchmarkProjectSuffix_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testSuffix = "Perf"; - - // Act - options.BenchmarkProjectSuffix = testSuffix; - - // Assert - Assert.Equal(testSuffix, options.BenchmarkProjectSuffix); - } - - [Fact] - public void UseDebugBuild_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - - // Act - options.AllowDebugBuild = true; - - // Assert - Assert.True(options.AllowDebugBuild); - } - - [Fact] - public void PostConfigureOptions_ShouldCombineRepositoryPathAndReportsFolder() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testRepoPath = @"C:\MyBenchmarks"; - var testReportsFolder = "output"; - options.RepositoryPath = testRepoPath; - options.RepositoryReportsFolder = testReportsFolder; - - // Act - options.PostConfigureOptions(); - - // Assert - var expectedPath = Path.Combine(testRepoPath, testReportsFolder); - Assert.Equal(expectedPath, options.Configuration.ArtifactsPath); - } - - [Fact] - public void ValidateOptions_ShouldNotThrow_WhenAllPropertiesAreValid() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - - // Act & Assert - var exception = Record.Exception(() => options.ValidateOptions()); - Assert.Null(exception); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenConfigurationIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - Configuration = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = string.Empty - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsWhitespace() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = " " - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryTuningFolder = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryTuningFolder = string.Empty - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsWhitespace() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryTuningFolder = " " - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryReportsFolder = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryReportsFolder = string.Empty - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsWhitespace() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryReportsFolder = " " - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - TargetFrameworkMoniker = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - TargetFrameworkMoniker = string.Empty - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsWhitespace() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - TargetFrameworkMoniker = " " - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - BenchmarkProjectSuffix = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - BenchmarkProjectSuffix = string.Empty - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsWhitespace() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - BenchmarkProjectSuffix = " " - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void Constructor_ShouldSetRepositoryPath_ToGitRootOrFallback() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.RepositoryPath); - Assert.NotEmpty(options.RepositoryPath); - - TestOutput.WriteLine($"RepositoryPath: {options.RepositoryPath}"); - } - - [Fact] - public void Constructor_ShouldSetConfiguration_WithMemoryDiagnoser() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.Configuration); - Assert.Contains(options.Configuration.GetDiagnosers(), d => d.GetType().Name.Contains("Memory")); - } - - [Fact] - public void Constructor_ShouldSetConfiguration_WithConsoleLogger() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.Configuration); - Assert.NotEmpty(options.Configuration.GetLoggers()); - } - - [Fact] - public void Constructor_ShouldSetConfiguration_WithMarkdownExporter() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.Configuration); - Assert.NotEmpty(options.Configuration.GetExporters()); - } - - [Fact] - public void Constructor_ShouldSetTargetFrameworkMoniker_ToCurrentRuntime() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.TargetFrameworkMoniker); - Assert.StartsWith("net", options.TargetFrameworkMoniker, StringComparison.OrdinalIgnoreCase); - - TestOutput.WriteLine($"TargetFrameworkMoniker: {options.TargetFrameworkMoniker}"); - } - - [Theory] - [InlineData("net8.0")] - [InlineData("net9.0")] - [InlineData("net10.0")] - public void TargetFrameworkMoniker_ShouldAcceptValidTfmFormats(string tfm) - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - - // Act - options.TargetFrameworkMoniker = tfm; - - // Assert - Assert.Equal(tfm, options.TargetFrameworkMoniker); - } - - [Fact] - public void PostConfigureOptions_ShouldPreserveOtherConfigurationSettings() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var originalLoggers = options.Configuration.GetLoggers(); - var originalExporters = options.Configuration.GetExporters(); - - // Act - options.PostConfigureOptions(); - - // Assert - Assert.Equal(originalLoggers.Count(), options.Configuration.GetLoggers().Count()); - Assert.Equal(originalExporters.Count(), options.Configuration.GetExporters().Count()); - } - - [Fact] - public void UseDebugBuild_ShouldDefaultToFalse() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.False(options.AllowDebugBuild); - } - - [Fact] - public void Slim_ShouldBeConfiguredJob() - { - // Act - var slimJob = BenchmarkWorkspaceOptions.Slim; - - // Assert - Assert.NotNull(slimJob); - Assert.Equal(1, slimJob.Run.WarmupCount); - Assert.Equal(15, slimJob.Run.MinIterationCount); - Assert.Equal(20, slimJob.Run.MaxIterationCount); - Assert.Equal(250, slimJob.Run.IterationTime.ToMilliseconds()); - - TestOutput.WriteLine($"Slim Job - WarmupCount: {slimJob.Run.WarmupCount}"); - TestOutput.WriteLine($"Slim Job - MinIterationCount: {slimJob.Run.MinIterationCount}"); - TestOutput.WriteLine($"Slim Job - MaxIterationCount: {slimJob.Run.MaxIterationCount}"); - TestOutput.WriteLine($"Slim Job - IterationTime: {slimJob.Run.IterationTime.ToMilliseconds()}ms"); - } - - [Fact] - public void PostConfigureOptions_ShouldNotOverwriteExistingArtifactsPath() - { - // Arrange - var existingPath = @"C:\ExistingArtifacts"; - var options = new BenchmarkWorkspaceOptions(); - - if (options.Configuration is ManualConfig manual) - { - manual.ArtifactsPath = existingPath; - } - else - { - options.Configuration = options.Configuration.WithArtifactsPath(existingPath); - } - - // Act - options.PostConfigureOptions(); - - // Assert - Assert.Equal(existingPath, options.Configuration.ArtifactsPath); - } - - [Fact] - public void PostConfigureOptions_ShouldWorkWithNonManualConfig() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testRepoPath = @"C:\TestRepo"; - var testReportsFolder = "reports"; - - // Create a non-ManualConfig by using WithArtifactsPath on DefaultConfig - options.Configuration = ManualConfig.CreateEmpty().WithArtifactsPath(""); - options.RepositoryPath = testRepoPath; - options.RepositoryReportsFolder = testReportsFolder; - - // Act - options.PostConfigureOptions(); - - // Assert - var expectedPath = Path.Combine(testRepoPath, testReportsFolder); - Assert.Equal(expectedPath, options.Configuration.ArtifactsPath); - } - - [Fact] - public void Configuration_ShouldHaveStatisticColumns() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - var config = options.Configuration as ManualConfig; - Assert.NotNull(config); - - // Check that column providers were added via the config - var columnProviders = options.Configuration.GetColumnProviders().ToList(); - Assert.NotEmpty(columnProviders); - - // Verify StatisticsColumnProvider is present (which would include our Median, Min, Max columns) - Assert.Contains(columnProviders, provider => provider.GetType().Name == "StatisticsColumnProvider"); - - // The configuration includes DefaultColumnProviders.Instance which adds standard providers - var providerNames = columnProviders.Select(p => p.GetType().Name).ToList(); - TestOutput.WriteLine($"Column Providers: {string.Join(", ", providerNames)}"); - } - - [Fact] - public void Configuration_ShouldHaveCustomSummaryStyle() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.Configuration.SummaryStyle); - Assert.Equal(36, options.Configuration.SummaryStyle.MaxParameterColumnWidth); - - TestOutput.WriteLine($"MaxParameterColumnWidth: {options.Configuration.SummaryStyle.MaxParameterColumnWidth}"); - } - - [Fact] - public void Configuration_ShouldHaveDisabledLogFile() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.True((options.Configuration.Options & ConfigOptions.DisableLogFile) == ConfigOptions.DisableLogFile); - } - - [Fact] - public void Configuration_ShouldHaveBuildTimeout() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.Equal(TimeSpan.FromMinutes(15), options.Configuration.BuildTimeout); - - TestOutput.WriteLine($"BuildTimeout: {options.Configuration.BuildTimeout}"); - } - - [Fact] - public void Configuration_ShouldHaveDefaultValidatorsAndAnalysers() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotEmpty(options.Configuration.GetValidators()); - Assert.NotEmpty(options.Configuration.GetAnalysers()); - - TestOutput.WriteLine($"Validators: {options.Configuration.GetValidators().Count()}"); - TestOutput.WriteLine($"Analysers: {options.Configuration.GetAnalysers().Count()}"); - } - - [Fact] - public void Configuration_ShouldHaveSlimJobAsDefault() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - var jobs = options.Configuration.GetJobs().ToList(); - Assert.Single(jobs); - Assert.True(jobs[0].Meta.IsDefault); - - TestOutput.WriteLine($"Default Job - WarmupCount: {jobs[0].Run.WarmupCount}"); - } - - [Fact] - public void SkipBenchmarksWithReports_ShouldDefaultToFalse() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.False(options.SkipBenchmarksWithReports); - } - - [Fact] - public void SkipBenchmarksWithReports_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - - // Act - options.SkipBenchmarksWithReports = true; - - // Assert - Assert.True(options.SkipBenchmarksWithReports); - } - - [Theory] - [InlineData(".NETFramework,Version=v1.1", "net11")] - [InlineData(".NETFramework,Version=v2.0", "net20")] - [InlineData(".NETFramework,Version=v3.5", "net35")] - [InlineData(".NETFramework,Version=v4.0", "net40")] - [InlineData(".NETFramework,Version=v4.0.3", "net403")] - [InlineData(".NETFramework,Version=v4.5.1", "net451")] - [InlineData(".NETFramework,Version=v4.7.2", "net472")] - [InlineData(".NETFramework,Version=v4.8", "net48")] - [InlineData(".NETFramework,Version=v4.8.1", "net481")] - public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETFramework(string frameworkName, string expectedTfm) - { - // Act + +namespace Codebelt.Extensions.BenchmarkDotNet; + +public class BenchmarkWorkspaceOptionsTest : Test +{ + public BenchmarkWorkspaceOptionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldInitializeWithDefaultValues() + { + // Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.RepositoryPath); + Assert.NotNull(options.Configuration); + Assert.NotNull(options.TargetFrameworkMoniker); + Assert.Equal(BenchmarkWorkspaceOptions.DefaultRepositoryTuningFolder, options.RepositoryTuningFolder); + Assert.Equal(BenchmarkWorkspaceOptions.DefaultRepositoryReportsFolder, options.RepositoryReportsFolder); + Assert.Equal(BenchmarkWorkspaceOptions.DefaultBenchmarkProjectSuffix, options.BenchmarkProjectSuffix); + Assert.False(options.AllowDebugBuild); + } + + [Fact] + public void DefaultRepositoryReportsFolder_ShouldBeReports() + { + // Assert + Assert.Equal("reports", BenchmarkWorkspaceOptions.DefaultRepositoryReportsFolder); + } + + [Fact] + public void DefaultRepositoryTuningFolder_ShouldBeTuning() + { + // Assert + Assert.Equal("tuning", BenchmarkWorkspaceOptions.DefaultRepositoryTuningFolder); + } + + [Fact] + public void DefaultBenchmarkProjectSuffix_ShouldBeBenchmarks() + { + // Assert + Assert.Equal("Benchmarks", BenchmarkWorkspaceOptions.DefaultBenchmarkProjectSuffix); + } + + [Fact] + public void RepositoryPath_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testPath = @"C:\TestRepo"; + + // Act + options.RepositoryPath = testPath; + + // Assert + Assert.Equal(testPath, options.RepositoryPath); + } + + [Fact] + public void Configuration_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var customConfig = ManualConfig.CreateEmpty(); + + // Act + options.Configuration = customConfig; + + // Assert + Assert.Same(customConfig, options.Configuration); + } + + [Fact] + public void TargetFrameworkMoniker_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testTfm = "net8.0"; + + // Act + options.TargetFrameworkMoniker = testTfm; + + // Assert + Assert.Equal(testTfm, options.TargetFrameworkMoniker); + } + + [Fact] + public void RepositoryTuningFolder_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testFolder = "custom-tuning"; + + // Act + options.RepositoryTuningFolder = testFolder; + + // Assert + Assert.Equal(testFolder, options.RepositoryTuningFolder); + } + + [Fact] + public void RepositoryReportsFolder_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testFolder = "custom-reports"; + + // Act + options.RepositoryReportsFolder = testFolder; + + // Assert + Assert.Equal(testFolder, options.RepositoryReportsFolder); + } + + [Fact] + public void BenchmarkProjectSuffix_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testSuffix = "Perf"; + + // Act + options.BenchmarkProjectSuffix = testSuffix; + + // Assert + Assert.Equal(testSuffix, options.BenchmarkProjectSuffix); + } + + [Fact] + public void UseDebugBuild_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act + options.AllowDebugBuild = true; + + // Assert + Assert.True(options.AllowDebugBuild); + } + + [Fact] + public void PostConfigureOptions_ShouldCombineRepositoryPathAndReportsFolder() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testRepoPath = @"C:\MyBenchmarks"; + var testReportsFolder = "output"; + options.RepositoryPath = testRepoPath; + options.RepositoryReportsFolder = testReportsFolder; + + // Act + options.PostConfigureOptions(); + + // Assert + var expectedPath = Path.Combine(testRepoPath, testReportsFolder); + Assert.Equal(expectedPath, options.Configuration.ArtifactsPath); + } + + [Fact] + public void ValidateOptions_ShouldNotThrow_WhenAllPropertiesAreValid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act & Assert + var exception = Record.Exception(() => options.ValidateOptions()); + Assert.Null(exception); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenConfigurationIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + Configuration = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryTuningFolder = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryTuningFolder = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryTuningFolder = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryReportsFolder = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryReportsFolder = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryReportsFolder = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + TargetFrameworkMoniker = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + TargetFrameworkMoniker = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + TargetFrameworkMoniker = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void Constructor_ShouldSetRepositoryPath_ToGitRootOrFallback() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.RepositoryPath); + Assert.NotEmpty(options.RepositoryPath); + + TestOutput.WriteLine($"RepositoryPath: {options.RepositoryPath}"); + } + + [Fact] + public void Constructor_ShouldSetConfiguration_WithMemoryDiagnoser() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.Configuration); + Assert.Contains(options.Configuration.GetDiagnosers(), d => d.GetType().Name.Contains("Memory")); + } + + [Fact] + public void Constructor_ShouldSetConfiguration_WithConsoleLogger() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.Configuration); + Assert.NotEmpty(options.Configuration.GetLoggers()); + } + + [Fact] + public void Constructor_ShouldSetConfiguration_WithMarkdownExporter() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.Configuration); + Assert.NotEmpty(options.Configuration.GetExporters()); + } + + [Fact] + public void Constructor_ShouldSetTargetFrameworkMoniker_ToCurrentRuntime() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.TargetFrameworkMoniker); + Assert.StartsWith("net", options.TargetFrameworkMoniker, StringComparison.OrdinalIgnoreCase); + + TestOutput.WriteLine($"TargetFrameworkMoniker: {options.TargetFrameworkMoniker}"); + } + + [Theory] + [InlineData("net8.0")] + [InlineData("net9.0")] + [InlineData("net10.0")] + public void TargetFrameworkMoniker_ShouldAcceptValidTfmFormats(string tfm) + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act + options.TargetFrameworkMoniker = tfm; + + // Assert + Assert.Equal(tfm, options.TargetFrameworkMoniker); + } + + [Fact] + public void PostConfigureOptions_ShouldPreserveOtherConfigurationSettings() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var originalLoggers = options.Configuration.GetLoggers(); + var originalExporters = options.Configuration.GetExporters(); + + // Act + options.PostConfigureOptions(); + + // Assert + Assert.Equal(originalLoggers.Count(), options.Configuration.GetLoggers().Count()); + Assert.Equal(originalExporters.Count(), options.Configuration.GetExporters().Count()); + } + + [Fact] + public void UseDebugBuild_ShouldDefaultToFalse() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.False(options.AllowDebugBuild); + } + + [Fact] + public void Slim_ShouldBeConfiguredJob() + { + // Act + var slimJob = BenchmarkWorkspaceOptions.Slim; + + // Assert + Assert.NotNull(slimJob); + Assert.Equal(1, slimJob.Run.WarmupCount); + Assert.Equal(15, slimJob.Run.MinIterationCount); + Assert.Equal(20, slimJob.Run.MaxIterationCount); + Assert.Equal(250, slimJob.Run.IterationTime.ToMilliseconds()); + + TestOutput.WriteLine($"Slim Job - WarmupCount: {slimJob.Run.WarmupCount}"); + TestOutput.WriteLine($"Slim Job - MinIterationCount: {slimJob.Run.MinIterationCount}"); + TestOutput.WriteLine($"Slim Job - MaxIterationCount: {slimJob.Run.MaxIterationCount}"); + TestOutput.WriteLine($"Slim Job - IterationTime: {slimJob.Run.IterationTime.ToMilliseconds()}ms"); + } + + [Fact] + public void PostConfigureOptions_ShouldNotOverwriteExistingArtifactsPath() + { + // Arrange + var existingPath = @"C:\ExistingArtifacts"; + var options = new BenchmarkWorkspaceOptions(); + + if (options.Configuration is ManualConfig manual) + { + manual.ArtifactsPath = existingPath; + } + else + { + options.Configuration = options.Configuration.WithArtifactsPath(existingPath); + } + + // Act + options.PostConfigureOptions(); + + // Assert + Assert.Equal(existingPath, options.Configuration.ArtifactsPath); + } + + [Fact] + public void PostConfigureOptions_ShouldWorkWithNonManualConfig() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testRepoPath = @"C:\TestRepo"; + var testReportsFolder = "reports"; + + // Create a non-ManualConfig by using WithArtifactsPath on DefaultConfig + options.Configuration = ManualConfig.CreateEmpty().WithArtifactsPath(""); + options.RepositoryPath = testRepoPath; + options.RepositoryReportsFolder = testReportsFolder; + + // Act + options.PostConfigureOptions(); + + // Assert + var expectedPath = Path.Combine(testRepoPath, testReportsFolder); + Assert.Equal(expectedPath, options.Configuration.ArtifactsPath); + } + + [Fact] + public void Configuration_ShouldHaveStatisticColumns() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + var config = options.Configuration as ManualConfig; + Assert.NotNull(config); + + // Check that column providers were added via the config + var columnProviders = options.Configuration.GetColumnProviders().ToList(); + Assert.NotEmpty(columnProviders); + + // Verify StatisticsColumnProvider is present (which would include our Median, Min, Max columns) + Assert.Contains(columnProviders, provider => provider.GetType().Name == "StatisticsColumnProvider"); + + // The configuration includes DefaultColumnProviders.Instance which adds standard providers + var providerNames = columnProviders.Select(p => p.GetType().Name).ToList(); + TestOutput.WriteLine($"Column Providers: {string.Join(", ", providerNames)}"); + } + + [Fact] + public void Configuration_ShouldHaveCustomSummaryStyle() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.Configuration.SummaryStyle); + Assert.Equal(36, options.Configuration.SummaryStyle.MaxParameterColumnWidth); + + TestOutput.WriteLine($"MaxParameterColumnWidth: {options.Configuration.SummaryStyle.MaxParameterColumnWidth}"); + } + + [Fact] + public void Configuration_ShouldHaveDisabledLogFile() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.True((options.Configuration.Options & ConfigOptions.DisableLogFile) == ConfigOptions.DisableLogFile); + } + + [Fact] + public void Configuration_ShouldHaveBuildTimeout() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.Equal(TimeSpan.FromMinutes(15), options.Configuration.BuildTimeout); + + TestOutput.WriteLine($"BuildTimeout: {options.Configuration.BuildTimeout}"); + } + + [Fact] + public void Configuration_ShouldHaveDefaultValidatorsAndAnalysers() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotEmpty(options.Configuration.GetValidators()); + Assert.NotEmpty(options.Configuration.GetAnalysers()); + + TestOutput.WriteLine($"Validators: {options.Configuration.GetValidators().Count()}"); + TestOutput.WriteLine($"Analysers: {options.Configuration.GetAnalysers().Count()}"); + } + + [Fact] + public void Configuration_ShouldHaveSlimJobAsDefault() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + var jobs = options.Configuration.GetJobs().ToList(); + Assert.Single(jobs); + Assert.True(jobs[0].Meta.IsDefault); + + TestOutput.WriteLine($"Default Job - WarmupCount: {jobs[0].Run.WarmupCount}"); + } + + [Fact] + public void SkipBenchmarksWithReports_ShouldDefaultToFalse() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.False(options.SkipBenchmarksWithReports); + } + + [Fact] + public void SkipBenchmarksWithReports_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act + options.SkipBenchmarksWithReports = true; + + // Assert + Assert.True(options.SkipBenchmarksWithReports); + } + + [Theory] + [InlineData(".NETFramework,Version=v1.1", "net11")] + [InlineData(".NETFramework,Version=v2.0", "net20")] + [InlineData(".NETFramework,Version=v3.5", "net35")] + [InlineData(".NETFramework,Version=v4.0", "net40")] + [InlineData(".NETFramework,Version=v4.0.3", "net403")] + [InlineData(".NETFramework,Version=v4.5.1", "net451")] + [InlineData(".NETFramework,Version=v4.7.2", "net472")] + [InlineData(".NETFramework,Version=v4.8", "net48")] + [InlineData(".NETFramework,Version=v4.8.1", "net481")] + public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETFramework(string frameworkName, string expectedTfm) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Equal(expectedTfm, result); - - TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); - } - - [Theory] - [InlineData(".NETStandard,Version=v1.0", "netstandard1.0")] - [InlineData(".NETStandard,Version=v1.6", "netstandard1.6")] - [InlineData(".NETStandard,Version=v2.0", "netstandard2.0")] - [InlineData(".NETStandard,Version=v2.1", "netstandard2.1")] - public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETStandard(string frameworkName, string expectedTfm) - { - // Act + + // Assert + Assert.Equal(expectedTfm, result); + + TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); + } + + [Theory] + [InlineData(".NETStandard,Version=v1.0", "netstandard1.0")] + [InlineData(".NETStandard,Version=v1.6", "netstandard1.6")] + [InlineData(".NETStandard,Version=v2.0", "netstandard2.0")] + [InlineData(".NETStandard,Version=v2.1", "netstandard2.1")] + public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETStandard(string frameworkName, string expectedTfm) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Equal(expectedTfm, result); - - TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); - } - - [Theory] - [InlineData(".NETCoreApp,Version=v1.0", "netcoreapp1.0")] - [InlineData(".NETCoreApp,Version=v2.1", "netcoreapp2.1")] - [InlineData(".NETCoreApp,Version=v3.1", "netcoreapp3.1")] - public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETCoreApp(string frameworkName, string expectedTfm) - { - // Act + + // Assert + Assert.Equal(expectedTfm, result); + + TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); + } + + [Theory] + [InlineData(".NETCoreApp,Version=v1.0", "netcoreapp1.0")] + [InlineData(".NETCoreApp,Version=v2.1", "netcoreapp2.1")] + [InlineData(".NETCoreApp,Version=v3.1", "netcoreapp3.1")] + public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETCoreApp(string frameworkName, string expectedTfm) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Equal(expectedTfm, result); - - TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); - } - - [Theory] - [InlineData(".NETCoreApp,Version=v5.0", "net5.0")] - [InlineData(".NETCoreApp,Version=v6.0", "net6.0")] - [InlineData(".NETCoreApp,Version=v8.0", "net8.0")] - [InlineData(".NETCoreApp,Version=v9.0", "net9.0")] - [InlineData(".NETCoreApp,Version=v10.0", "net10.0")] - public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForModernNet(string frameworkName, string expectedTfm) - { - // Act + + // Assert + Assert.Equal(expectedTfm, result); + + TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); + } + + [Theory] + [InlineData(".NETCoreApp,Version=v5.0", "net5.0")] + [InlineData(".NETCoreApp,Version=v6.0", "net6.0")] + [InlineData(".NETCoreApp,Version=v8.0", "net8.0")] + [InlineData(".NETCoreApp,Version=v9.0", "net9.0")] + [InlineData(".NETCoreApp,Version=v10.0", "net10.0")] + public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForModernNet(string frameworkName, string expectedTfm) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Equal(expectedTfm, result); - - TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - public void ParseTargetFrameworkMoniker_ShouldReturnNull_WhenFrameworkNameIsNullOrEmpty(string frameworkName) - { - // Act + + // Assert + Assert.Equal(expectedTfm, result); + + TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void ParseTargetFrameworkMoniker_ShouldReturnNull_WhenFrameworkNameIsNullOrEmpty(string frameworkName) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Null(result); - } - - [Theory] - [InlineData("not-a-framework-name")] - [InlineData(".NETSomethingUnknown,Version=v1.0")] - public void ParseTargetFrameworkMoniker_ShouldReturnNull_WhenFrameworkNameIsUnrecognised(string frameworkName) - { - // Act + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("not-a-framework-name")] + [InlineData(".NETSomethingUnknown,Version=v1.0")] + public void ParseTargetFrameworkMoniker_ShouldReturnNull_WhenFrameworkNameIsUnrecognised(string frameworkName) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Null(result); - + + // Assert + Assert.Null(result); + TestOutput.WriteLine($"Unrecognised framework: {frameworkName} → null"); } diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs index dd9e49a..0d8b94c 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs @@ -1,1001 +1,1001 @@ -using BenchmarkDotNet.Configs; -using Codebelt.Extensions.Xunit; -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using Cuemon; -using Cuemon.Reflection; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -public class BenchmarkWorkspaceTest : Test -{ - private static readonly bool IsDebugBuild = GetBuildConfiguration(); - - public BenchmarkWorkspaceTest(ITestOutputHelper output) : base(output) - { - } - - private static bool GetBuildConfiguration() - { - return Decorator.Enclose(typeof(BenchmarkWorkspaceTest).Assembly).IsDebugBuild(); - } - - [Fact] - public void Constructor_ShouldThrowArgumentNullException_WhenOptionsIsNull() - { - // Arrange - BenchmarkWorkspaceOptions options = null; - - // Act & Assert - Assert.Throws(() => new BenchmarkWorkspace(options)); - } - - [Fact] - public void Constructor_ShouldThrowArgumentException_WhenOptionsAreInvalid() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = null - }; - - // Act & Assert - Assert.Throws(() => new BenchmarkWorkspace(options)); - } - - [Fact] - public void Constructor_ShouldSucceed_WhenOptionsAreValid() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - - // Act - var workspace = new BenchmarkWorkspace(options); - - // Assert - Assert.NotNull(workspace); - } - - [Fact] - public void Constructor_ShouldEnableDisableOptimizationsValidator_WhenUseDebugBuildIsTrue() - { - // Arrange - var config = ManualConfig.CreateEmpty(); - var options = new BenchmarkWorkspaceOptions - { - Configuration = config, - AllowDebugBuild = true - }; - - // Act - var workspace = new BenchmarkWorkspace(options); - - // Assert - Assert.NotNull(workspace); - var manualConfig = options.Configuration as ManualConfig; - Assert.NotNull(manualConfig); - Assert.True(manualConfig.Options.HasFlag(ConfigOptions.DisableOptimizationsValidator)); - } - - [Fact] - public void Constructor_ShouldNotModifyConfiguration_WhenUseDebugBuildIsFalse() - { - // Arrange - var config = ManualConfig.CreateEmpty(); - var originalOptions = config.Options; - var options = new BenchmarkWorkspaceOptions - { - Configuration = config, - AllowDebugBuild = false - }; - - // Act - var workspace = new BenchmarkWorkspace(options); - - // Assert - Assert.NotNull(workspace); - var manualConfig = options.Configuration as ManualConfig; - Assert.NotNull(manualConfig); - Assert.Equal(originalOptions, manualConfig.Options); - } - - [Fact] - public void Constructor_ShouldNotModifyConfiguration_WhenConfigurationIsNotManualConfig() - { - // Arrange - var config = DefaultConfig.Instance; - var options = new BenchmarkWorkspaceOptions - { - Configuration = config - }; - - // Act - var workspace = new BenchmarkWorkspace(options); - - // Assert - Assert.NotNull(workspace); - Assert.Same(config, options.Configuration); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldThrowInvalidOperationException_WhenNoAssembliesFound() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - RepositoryTuningFolder = "tuning", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act & Assert - var exception = Assert.Throws(() => workspace.LoadBenchmarkAssemblies()); - - TestOutput.WriteLine($"{exception.Message}"); - - Assert.Contains("No assemblies were loaded", exception.Message); - Assert.Contains(IsDebugBuild ? "Debug" : "Release", exception.Message); - Assert.Contains("net10.0", exception.Message); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldCreateTuningDirectory_WhenItDoesNotExist() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - RepositoryTuningFolder = "tuning", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - - var workspace = new BenchmarkWorkspace(options); - var expectedTuningPath = Path.Combine(tempPath, "tuning"); - - // Act - try - { - workspace.LoadBenchmarkAssemblies(); - } - catch (InvalidOperationException) - { - // Expected when no assemblies found - } - - // Assert - Assert.True(Directory.Exists(expectedTuningPath)); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldReturnLoadedAssemblies_WhenMatchingAssembliesExist() - { - // Arrange - var options = new BenchmarkWorkspaceOptions() - { - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.NotEmpty(assemblies); - Assert.All(assemblies, assembly => Assert.NotNull(assembly)); - - TestOutput.WriteLine($"Current build configuration: {(IsDebugBuild ? "Debug" : "Release")}"); - TestOutput.WriteLine($"Loaded {assemblies.Length} benchmark assemblies:"); - foreach (var assembly in assemblies) - { - TestOutput.WriteLine($" - {assembly.GetName().Name}"); - } - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldFilterByBuildConfiguration() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - var expectedBuildConfig = IsDebugBuild ? "Debug" : "Release"; - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.All(assemblies, assembly => - { - var location = assembly.Location; - TestOutput.WriteLine($"Assembly location: {location}"); - Assert.Contains(expectedBuildConfig, location, StringComparison.OrdinalIgnoreCase); - }); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldFilterByTargetFrameworkMoniker() - { - // Arrange - var options = new BenchmarkWorkspaceOptions() - { - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - var expectedTfm = options.TargetFrameworkMoniker; - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.All(assemblies, assembly => - { - var location = assembly.Location; - TestOutput.WriteLine($"Assembly location: {location}"); - Assert.Contains(expectedTfm, location, StringComparison.OrdinalIgnoreCase); - }); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldFilterByBenchmarkProjectSuffix() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - BenchmarkProjectSuffix = "Benchmarks", - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.All(assemblies, assembly => - { - var name = assembly.GetName().Name; - TestOutput.WriteLine($"Assembly name: {name}"); - Assert.Contains("Benchmarks", name, StringComparison.OrdinalIgnoreCase); - }); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldNotLoadDuplicateAssemblies() - { - // Arrange - var options = new BenchmarkWorkspaceOptions() - { - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - var uniqueAssemblies = assemblies.Distinct().ToArray(); - Assert.Equal(assemblies.Length, uniqueAssemblies.Length); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldReuseAlreadyLoadedAssemblies() - { - // Arrange - var options = new BenchmarkWorkspaceOptions() - { - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - var firstLoad = workspace.LoadBenchmarkAssemblies(); - var secondLoad = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(firstLoad); - Assert.NotNull(secondLoad); - Assert.All(firstLoad, firstAssembly => - { - var matchingSecondAssembly = secondLoad.FirstOrDefault(second => - AssemblyName.ReferenceMatchesDefinition(second.GetName(), firstAssembly.GetName())); - Assert.NotNull(matchingSecondAssembly); - }); - } - - [Fact] - public void PostProcessArtifacts_ShouldMoveFilesFromResultsDirectory() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - - Directory.CreateDirectory(resultsDir); - - var testFile1 = Path.Combine(resultsDir, "test1.txt"); - var testFile2 = Path.Combine(resultsDir, "test2.md"); - File.WriteAllText(testFile1, "Test content 1"); - File.WriteAllText(testFile2, "Test content 2"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act - workspace.PostProcessArtifacts(); - - // Assert - Assert.False(Directory.Exists(resultsDir)); - Assert.True(Directory.Exists(tuningDir)); - Assert.True(File.Exists(Path.Combine(tuningDir, "test1.txt"))); - Assert.True(File.Exists(Path.Combine(tuningDir, "test2.md"))); - - TestOutput.WriteLine($"Files moved successfully to: {tuningDir}"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void PostProcessArtifacts_ShouldDoNothing_WhenResultsDirectoryDoesNotExist() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - Directory.CreateDirectory(artifactsPath); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act - var exception = Record.Exception(() => workspace.PostProcessArtifacts()); - - // Assert - Assert.Null(exception); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void PostProcessArtifacts_ShouldOverwriteExistingFiles() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - - Directory.CreateDirectory(resultsDir); - Directory.CreateDirectory(tuningDir); - - var sourceFile = Path.Combine(resultsDir, "test.txt"); - var targetFile = Path.Combine(tuningDir, "test.txt"); - - File.WriteAllText(sourceFile, "New content"); - File.WriteAllText(targetFile, "Old content"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act - workspace.PostProcessArtifacts(); - - // Assert - Assert.False(Directory.Exists(resultsDir)); - Assert.True(File.Exists(targetFile)); - var content = File.ReadAllText(targetFile); - Assert.Equal("New content", content); - - TestOutput.WriteLine($"File successfully overwritten at: {targetFile}"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void PostProcessArtifacts_ShouldDeleteResultsDirectoryRecursively() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - var subDir = Path.Combine(resultsDir, "subdir"); - - Directory.CreateDirectory(subDir); - File.WriteAllText(Path.Combine(resultsDir, "file1.txt"), "Content 1"); - File.WriteAllText(Path.Combine(subDir, "file2.txt"), "Content 2"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act - workspace.PostProcessArtifacts(); - - // Assert - Assert.False(Directory.Exists(resultsDir)); - Assert.False(Directory.Exists(subDir)); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void PostProcessArtifacts_ShouldCreateTuningDirectory_WhenItDoesNotExist() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - - Directory.CreateDirectory(resultsDir); - File.WriteAllText(Path.Combine(resultsDir, "test.txt"), "Content"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - var workspace = new BenchmarkWorkspace(options); - var expectedTuningPath = Path.Combine(artifactsPath, "tuning"); - - // Act - workspace.PostProcessArtifacts(); - - // Assert - Assert.True(Directory.Exists(expectedTuningPath)); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void BenchmarkWorkspace_ShouldImplementIBenchmarkWorkspace() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - var workspace = new BenchmarkWorkspace(options); - - // Assert - Assert.IsAssignableFrom(workspace); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldHandleAssemblyLoadFailuresGracefully() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - var buildConfig = IsDebugBuild ? "Debug" : "Release"; - try - { - Directory.CreateDirectory(tempPath); - var tuningDir = Path.Combine(tempPath, "tuning"); - var buildDir = Path.Combine(tuningDir, "bin", buildConfig, "net10.0"); - Directory.CreateDirectory(buildDir); - - // Create an invalid DLL file - var invalidDll = Path.Combine(buildDir, "Invalid.Benchmarks.dll"); - File.WriteAllText(invalidDll, "This is not a valid assembly"); - - // Create a valid assembly reference - var validDll = Path.Combine(buildDir, "Valid.Benchmarks.dll"); - File.Copy(Assembly.GetExecutingAssembly().Location, validDll, true); - - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - RepositoryTuningFolder = "tuning", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.NotEmpty(assemblies); - Assert.All(assemblies, assembly => Assert.NotNull(assembly)); - - TestOutput.WriteLine($"Build configuration: {buildConfig}"); - TestOutput.WriteLine($"Successfully loaded {assemblies.Length} valid assemblies while skipping invalid ones"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void GetReportsResultsPath_ShouldReturnCorrectPath_WhenOptionsAreValid() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config - }; - - // Act - var resultsPath = BenchmarkWorkspace.GetReportsResultsPath(options); - - // Assert - var expectedPath = Path.Combine(artifactsPath, "results"); - Assert.Equal(expectedPath, resultsPath); - - TestOutput.WriteLine($"Results path: {resultsPath}"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void GetReportsResultsPath_ShouldThrowArgumentException_WhenOptionsAreInvalid() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = null - }; - - // Act & Assert - Assert.Throws(() => BenchmarkWorkspace.GetReportsResultsPath(options)); - } - - [Fact] - public void GetReportsResultsPath_ShouldThrowArgumentNullException_WhenOptionsIsNull() - { - // Arrange - BenchmarkWorkspaceOptions options = null; - - // Act & Assert - Assert.Throws(() => BenchmarkWorkspace.GetReportsResultsPath(options)); - } - - [Fact] - public void GetReportsTuningPath_ShouldReturnCorrectPath_WhenOptionsAreValid() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - // Act - var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); - - // Assert - var expectedPath = Path.Combine(artifactsPath, "tuning"); - Assert.Equal(expectedPath, tuningPath); - - TestOutput.WriteLine($"Tuning path: {tuningPath}"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void GetReportsTuningPath_ShouldThrowArgumentException_WhenOptionsAreInvalid() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = null - }; - - // Act & Assert - Assert.Throws(() => BenchmarkWorkspace.GetReportsTuningPath(options)); - } - - [Fact] - public void GetReportsTuningPath_ShouldThrowArgumentNullException_WhenOptionsIsNull() - { - // Arrange - BenchmarkWorkspaceOptions options = null; - - // Act & Assert - Assert.Throws(() => BenchmarkWorkspace.GetReportsTuningPath(options)); - } - - [Fact] - public void GetReportsTuningPath_ShouldUseCustomTuningFolder_WhenSpecified() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var customTuningFolder = "custom-tuning"; - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = customTuningFolder - }; - - // Act - var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); - - // Assert - var expectedPath = Path.Combine(artifactsPath, customTuningFolder); - Assert.Equal(expectedPath, tuningPath); - Assert.Contains(customTuningFolder, tuningPath); - - TestOutput.WriteLine($"Custom tuning path: {tuningPath}"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void GetReportsResultsPath_AndGetReportsTuningPath_ShouldReturnDifferentPaths() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - AllowDebugBuild = IsDebugBuild - }; - - // Act - var resultsPath = BenchmarkWorkspace.GetReportsResultsPath(options); - var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); - - // Assert - Assert.NotEqual(resultsPath, tuningPath); - Assert.Contains("results", resultsPath); - Assert.Contains("tuning", tuningPath); - - TestOutput.WriteLine($"Results path: {resultsPath}"); - TestOutput.WriteLine($"Tuning path: {tuningPath}"); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldSkipEmptyFileName_WhenDllHasNoStem() - { - // Arrange – place a file whose stem is empty so that Path.GetFileNameWithoutExtension returns "" - // which exercises the IsNullOrEmpty(simpleName) guard in UpdateAssemblyLookup (lines 169-170). - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - var buildConfig = IsDebugBuild ? "Debug" : "Release"; - try - { - Directory.CreateDirectory(tempPath); - var tuningDir = Path.Combine(tempPath, "tuning"); - var buildDir = Path.Combine(tuningDir, "bin", buildConfig, "net10.0"); - Directory.CreateDirectory(buildDir); - - // A file named ".dll" has an empty stem – Path.GetFileNameWithoutExtension(".dll") == "" - File.WriteAllText(Path.Combine(buildDir, ".dll"), "not-an-assembly"); - - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - RepositoryTuningFolder = "tuning", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - var workspace = new BenchmarkWorkspace(options); - - // Act – will throw because there are no real assemblies; that's expected - var exception = Record.Exception(() => workspace.LoadBenchmarkAssemblies()); - - // Assert – the important thing is that no unhandled exception from UpdateAssemblyLookup occurred - Assert.IsType(exception); - Assert.Contains("No assemblies were loaded", exception.Message); - - TestOutput.WriteLine("Empty-stem .dll file was silently skipped as expected."); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void PostProcessArtifacts_ShouldSkipBenchmarkRunFiles_WhenResultsDirContainsThem() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - - Directory.CreateDirectory(resultsDir); - - // BenchmarkRun-prefixed files should NOT be moved - File.WriteAllText(Path.Combine(resultsDir, "BenchmarkRun-20240101-120000.log"), "run log"); - // Regular report files SHOULD be moved - File.WriteAllText(Path.Combine(resultsDir, "MyBenchmark-report.md"), "report"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - workspace.PostProcessArtifacts(); - - // Assert – the report file was moved but the BenchmarkRun log was NOT - Assert.False(Directory.Exists(resultsDir), "results directory should be deleted"); - Assert.True(File.Exists(Path.Combine(tuningDir, "MyBenchmark-report.md"))); - Assert.False(File.Exists(Path.Combine(tuningDir, "BenchmarkRun-20240101-120000.log"))); - - TestOutput.WriteLine("BenchmarkRun files correctly excluded from PostProcessArtifacts move."); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldSkipDuplicateAssembly_WhenSameIdentityAppearsAtTwoPaths() - { - // This test exercises lines 141-142 (the duplicate-assembly-in-queue guard): - // - // if (assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName))) - // { - // continue; // ← lines 141-142 - // } - // - // The guard fires when: - // 1. 'alreadyLoaded' (snapshot taken BEFORE the loop) does NOT contain the assembly, - // 2. the loop adds path1 of the assembly to 'assemblies', - // 3. the loop then encounters path2 with the SAME identity – - // 'alreadyLoaded' still has no entry (same snapshot), so the first `if` is skipped, - // but `assemblies.Any(...)` is TRUE → duplicate guard fires. - // - // To guarantee the assembly is never in the initial AppDomain snapshot we generate a - // brand-new assembly with a unique GUID-based name via PersistedAssemblyBuilder. - - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - var buildConfig = IsDebugBuild ? "Debug" : "Release"; - try - { - // Create two subdirectories that both match the build segment - var proj1Dir = Path.Combine(tempPath, "tuning", "proj1", "bin", buildConfig, "net10.0"); - var proj2Dir = Path.Combine(tempPath, "tuning", "proj2", "bin", buildConfig, "net10.0"); - Directory.CreateDirectory(proj1Dir); - Directory.CreateDirectory(proj2Dir); - - // Build a minimal in-memory assembly with a guaranteed-unique name - var uniqueId = Guid.NewGuid().ToString("N"); - var asmName = new AssemblyName($"Unique{uniqueId}.Benchmarks") { Version = new Version(1, 0, 0, 0) }; - - var persistedBuilder = new System.Reflection.Emit.PersistedAssemblyBuilder(asmName, typeof(object).Assembly); - persistedBuilder.DefineDynamicModule("main") - .DefineType("Placeholder", System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class) - .CreateType(); - - // Save to path1, then copy to path2 so both carry the SAME assembly identity - var fileName = $"Unique{uniqueId}.Benchmarks.dll"; - var path1 = Path.Combine(proj1Dir, fileName); - var path2 = Path.Combine(proj2Dir, fileName); - persistedBuilder.Save(path1); - File.Copy(path1, path2); - - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - RepositoryTuningFolder = "tuning", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - var workspace = new BenchmarkWorkspace(options); - - // Act – both paths match, one is loaded, the second hits the duplicate guard - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert – only ONE assembly loaded despite two matching paths - Assert.NotNull(assemblies); - Assert.Equal(1, assemblies.Length); - Assert.Contains(assemblies, a => a.GetName().Name == asmName.Name); - - TestOutput.WriteLine($"Loaded {assemblies.Length} assembly (duplicate suppressed): {assemblies[0].GetName().Name}"); - } - finally - { - // Unload assemblies and release file locks before cleanup on Windows - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - - // On Windows, loaded DLLs may still hold locks; retry deletion with delay - if (Directory.Exists(tempPath)) - { - const int maxRetries = 5; - const int delayMs = 200; - var lastException = (Exception)null; - for (int i = 0; i < maxRetries; i++) - { - try - { - Directory.Delete(tempPath, true); - break; - } - catch (UnauthorizedAccessException ex) - { - lastException = ex; - if (i < maxRetries - 1) - { - System.Threading.Thread.Sleep(delayMs); - } - } - } - // If we still can't delete after retries, log but don't fail the test - // The OS will clean up the temp directory eventually - if (Directory.Exists(tempPath)) - { - TestOutput.WriteLine($"Warning: Could not delete temp directory {tempPath}"); - } - } - } - } -} +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.Xunit; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Cuemon; +using Cuemon.Reflection; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +public class BenchmarkWorkspaceTest : Test +{ + private static readonly bool IsDebugBuild = GetBuildConfiguration(); + + public BenchmarkWorkspaceTest(ITestOutputHelper output) : base(output) + { + } + + private static bool GetBuildConfiguration() + { + return Decorator.Enclose(typeof(BenchmarkWorkspaceTest).Assembly).IsDebugBuild(); + } + + [Fact] + public void Constructor_ShouldThrowArgumentNullException_WhenOptionsIsNull() + { + // Arrange + BenchmarkWorkspaceOptions options = null; + + // Act & Assert + Assert.Throws(() => new BenchmarkWorkspace(options)); + } + + [Fact] + public void Constructor_ShouldThrowArgumentException_WhenOptionsAreInvalid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = null + }; + + // Act & Assert + Assert.Throws(() => new BenchmarkWorkspace(options)); + } + + [Fact] + public void Constructor_ShouldSucceed_WhenOptionsAreValid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + } + + [Fact] + public void Constructor_ShouldEnableDisableOptimizationsValidator_WhenUseDebugBuildIsTrue() + { + // Arrange + var config = ManualConfig.CreateEmpty(); + var options = new BenchmarkWorkspaceOptions + { + Configuration = config, + AllowDebugBuild = true + }; + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + var manualConfig = options.Configuration as ManualConfig; + Assert.NotNull(manualConfig); + Assert.True(manualConfig.Options.HasFlag(ConfigOptions.DisableOptimizationsValidator)); + } + + [Fact] + public void Constructor_ShouldNotModifyConfiguration_WhenUseDebugBuildIsFalse() + { + // Arrange + var config = ManualConfig.CreateEmpty(); + var originalOptions = config.Options; + var options = new BenchmarkWorkspaceOptions + { + Configuration = config, + AllowDebugBuild = false + }; + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + var manualConfig = options.Configuration as ManualConfig; + Assert.NotNull(manualConfig); + Assert.Equal(originalOptions, manualConfig.Options); + } + + [Fact] + public void Constructor_ShouldNotModifyConfiguration_WhenConfigurationIsNotManualConfig() + { + // Arrange + var config = DefaultConfig.Instance; + var options = new BenchmarkWorkspaceOptions + { + Configuration = config + }; + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + Assert.Same(config, options.Configuration); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldThrowInvalidOperationException_WhenNoAssembliesFound() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act & Assert + var exception = Assert.Throws(() => workspace.LoadBenchmarkAssemblies()); + + TestOutput.WriteLine($"{exception.Message}"); + + Assert.Contains("No assemblies were loaded", exception.Message); + Assert.Contains(IsDebugBuild ? "Debug" : "Release", exception.Message); + Assert.Contains("net10.0", exception.Message); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldCreateTuningDirectory_WhenItDoesNotExist() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + + var workspace = new BenchmarkWorkspace(options); + var expectedTuningPath = Path.Combine(tempPath, "tuning"); + + // Act + try + { + workspace.LoadBenchmarkAssemblies(); + } + catch (InvalidOperationException) + { + // Expected when no assemblies found + } + + // Assert + Assert.True(Directory.Exists(expectedTuningPath)); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldReturnLoadedAssemblies_WhenMatchingAssembliesExist() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.NotEmpty(assemblies); + Assert.All(assemblies, assembly => Assert.NotNull(assembly)); + + TestOutput.WriteLine($"Current build configuration: {(IsDebugBuild ? "Debug" : "Release")}"); + TestOutput.WriteLine($"Loaded {assemblies.Length} benchmark assemblies:"); + foreach (var assembly in assemblies) + { + TestOutput.WriteLine($" - {assembly.GetName().Name}"); + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldFilterByBuildConfiguration() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + var expectedBuildConfig = IsDebugBuild ? "Debug" : "Release"; + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.All(assemblies, assembly => + { + var location = assembly.Location; + TestOutput.WriteLine($"Assembly location: {location}"); + Assert.Contains(expectedBuildConfig, location, StringComparison.OrdinalIgnoreCase); + }); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldFilterByTargetFrameworkMoniker() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + var expectedTfm = options.TargetFrameworkMoniker; + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.All(assemblies, assembly => + { + var location = assembly.Location; + TestOutput.WriteLine($"Assembly location: {location}"); + Assert.Contains(expectedTfm, location, StringComparison.OrdinalIgnoreCase); + }); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldFilterByBenchmarkProjectSuffix() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = "Benchmarks", + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.All(assemblies, assembly => + { + var name = assembly.GetName().Name; + TestOutput.WriteLine($"Assembly name: {name}"); + Assert.Contains("Benchmarks", name, StringComparison.OrdinalIgnoreCase); + }); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldNotLoadDuplicateAssemblies() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + var uniqueAssemblies = assemblies.Distinct().ToArray(); + Assert.Equal(assemblies.Length, uniqueAssemblies.Length); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldReuseAlreadyLoadedAssemblies() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var firstLoad = workspace.LoadBenchmarkAssemblies(); + var secondLoad = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(firstLoad); + Assert.NotNull(secondLoad); + Assert.All(firstLoad, firstAssembly => + { + var matchingSecondAssembly = secondLoad.FirstOrDefault(second => + AssemblyName.ReferenceMatchesDefinition(second.GetName(), firstAssembly.GetName())); + Assert.NotNull(matchingSecondAssembly); + }); + } + + [Fact] + public void PostProcessArtifacts_ShouldMoveFilesFromResultsDirectory() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + + Directory.CreateDirectory(resultsDir); + + var testFile1 = Path.Combine(resultsDir, "test1.txt"); + var testFile2 = Path.Combine(resultsDir, "test2.md"); + File.WriteAllText(testFile1, "Test content 1"); + File.WriteAllText(testFile2, "Test content 2"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.True(Directory.Exists(tuningDir)); + Assert.True(File.Exists(Path.Combine(tuningDir, "test1.txt"))); + Assert.True(File.Exists(Path.Combine(tuningDir, "test2.md"))); + + TestOutput.WriteLine($"Files moved successfully to: {tuningDir}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldDoNothing_WhenResultsDirectoryDoesNotExist() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + Directory.CreateDirectory(artifactsPath); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + var exception = Record.Exception(() => workspace.PostProcessArtifacts()); + + // Assert + Assert.Null(exception); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldOverwriteExistingFiles() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + + Directory.CreateDirectory(resultsDir); + Directory.CreateDirectory(tuningDir); + + var sourceFile = Path.Combine(resultsDir, "test.txt"); + var targetFile = Path.Combine(tuningDir, "test.txt"); + + File.WriteAllText(sourceFile, "New content"); + File.WriteAllText(targetFile, "Old content"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.True(File.Exists(targetFile)); + var content = File.ReadAllText(targetFile); + Assert.Equal("New content", content); + + TestOutput.WriteLine($"File successfully overwritten at: {targetFile}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldDeleteResultsDirectoryRecursively() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var subDir = Path.Combine(resultsDir, "subdir"); + + Directory.CreateDirectory(subDir); + File.WriteAllText(Path.Combine(resultsDir, "file1.txt"), "Content 1"); + File.WriteAllText(Path.Combine(subDir, "file2.txt"), "Content 2"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.False(Directory.Exists(subDir)); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldCreateTuningDirectory_WhenItDoesNotExist() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + + Directory.CreateDirectory(resultsDir); + File.WriteAllText(Path.Combine(resultsDir, "test.txt"), "Content"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + var expectedTuningPath = Path.Combine(artifactsPath, "tuning"); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.True(Directory.Exists(expectedTuningPath)); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void BenchmarkWorkspace_ShouldImplementIBenchmarkWorkspace() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.IsAssignableFrom(workspace); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldHandleAssemblyLoadFailuresGracefully() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var buildConfig = IsDebugBuild ? "Debug" : "Release"; + try + { + Directory.CreateDirectory(tempPath); + var tuningDir = Path.Combine(tempPath, "tuning"); + var buildDir = Path.Combine(tuningDir, "bin", buildConfig, "net10.0"); + Directory.CreateDirectory(buildDir); + + // Create an invalid DLL file + var invalidDll = Path.Combine(buildDir, "Invalid.Benchmarks.dll"); + File.WriteAllText(invalidDll, "This is not a valid assembly"); + + // Create a valid assembly reference + var validDll = Path.Combine(buildDir, "Valid.Benchmarks.dll"); + File.Copy(Assembly.GetExecutingAssembly().Location, validDll, true); + + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.NotEmpty(assemblies); + Assert.All(assemblies, assembly => Assert.NotNull(assembly)); + + TestOutput.WriteLine($"Build configuration: {buildConfig}"); + TestOutput.WriteLine($"Successfully loaded {assemblies.Length} valid assemblies while skipping invalid ones"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void GetReportsResultsPath_ShouldReturnCorrectPath_WhenOptionsAreValid() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config + }; + + // Act + var resultsPath = BenchmarkWorkspace.GetReportsResultsPath(options); + + // Assert + var expectedPath = Path.Combine(artifactsPath, "results"); + Assert.Equal(expectedPath, resultsPath); + + TestOutput.WriteLine($"Results path: {resultsPath}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void GetReportsResultsPath_ShouldThrowArgumentException_WhenOptionsAreInvalid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = null + }; + + // Act & Assert + Assert.Throws(() => BenchmarkWorkspace.GetReportsResultsPath(options)); + } + + [Fact] + public void GetReportsResultsPath_ShouldThrowArgumentNullException_WhenOptionsIsNull() + { + // Arrange + BenchmarkWorkspaceOptions options = null; + + // Act & Assert + Assert.Throws(() => BenchmarkWorkspace.GetReportsResultsPath(options)); + } + + [Fact] + public void GetReportsTuningPath_ShouldReturnCorrectPath_WhenOptionsAreValid() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + // Act + var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); + + // Assert + var expectedPath = Path.Combine(artifactsPath, "tuning"); + Assert.Equal(expectedPath, tuningPath); + + TestOutput.WriteLine($"Tuning path: {tuningPath}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void GetReportsTuningPath_ShouldThrowArgumentException_WhenOptionsAreInvalid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = null + }; + + // Act & Assert + Assert.Throws(() => BenchmarkWorkspace.GetReportsTuningPath(options)); + } + + [Fact] + public void GetReportsTuningPath_ShouldThrowArgumentNullException_WhenOptionsIsNull() + { + // Arrange + BenchmarkWorkspaceOptions options = null; + + // Act & Assert + Assert.Throws(() => BenchmarkWorkspace.GetReportsTuningPath(options)); + } + + [Fact] + public void GetReportsTuningPath_ShouldUseCustomTuningFolder_WhenSpecified() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var customTuningFolder = "custom-tuning"; + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = customTuningFolder + }; + + // Act + var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); + + // Assert + var expectedPath = Path.Combine(artifactsPath, customTuningFolder); + Assert.Equal(expectedPath, tuningPath); + Assert.Contains(customTuningFolder, tuningPath); + + TestOutput.WriteLine($"Custom tuning path: {tuningPath}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void GetReportsResultsPath_AndGetReportsTuningPath_ShouldReturnDifferentPaths() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + AllowDebugBuild = IsDebugBuild + }; + + // Act + var resultsPath = BenchmarkWorkspace.GetReportsResultsPath(options); + var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); + + // Assert + Assert.NotEqual(resultsPath, tuningPath); + Assert.Contains("results", resultsPath); + Assert.Contains("tuning", tuningPath); + + TestOutput.WriteLine($"Results path: {resultsPath}"); + TestOutput.WriteLine($"Tuning path: {tuningPath}"); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldSkipEmptyFileName_WhenDllHasNoStem() + { + // Arrange – place a file whose stem is empty so that Path.GetFileNameWithoutExtension returns "" + // which exercises the IsNullOrEmpty(simpleName) guard in UpdateAssemblyLookup (lines 169-170). + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var buildConfig = IsDebugBuild ? "Debug" : "Release"; + try + { + Directory.CreateDirectory(tempPath); + var tuningDir = Path.Combine(tempPath, "tuning"); + var buildDir = Path.Combine(tuningDir, "bin", buildConfig, "net10.0"); + Directory.CreateDirectory(buildDir); + + // A file named ".dll" has an empty stem – Path.GetFileNameWithoutExtension(".dll") == "" + File.WriteAllText(Path.Combine(buildDir, ".dll"), "not-an-assembly"); + + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + var workspace = new BenchmarkWorkspace(options); + + // Act – will throw because there are no real assemblies; that's expected + var exception = Record.Exception(() => workspace.LoadBenchmarkAssemblies()); + + // Assert – the important thing is that no unhandled exception from UpdateAssemblyLookup occurred + Assert.IsType(exception); + Assert.Contains("No assemblies were loaded", exception.Message); + + TestOutput.WriteLine("Empty-stem .dll file was silently skipped as expected."); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldSkipBenchmarkRunFiles_WhenResultsDirContainsThem() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + + Directory.CreateDirectory(resultsDir); + + // BenchmarkRun-prefixed files should NOT be moved + File.WriteAllText(Path.Combine(resultsDir, "BenchmarkRun-20240101-120000.log"), "run log"); + // Regular report files SHOULD be moved + File.WriteAllText(Path.Combine(resultsDir, "MyBenchmark-report.md"), "report"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert – the report file was moved but the BenchmarkRun log was NOT + Assert.False(Directory.Exists(resultsDir), "results directory should be deleted"); + Assert.True(File.Exists(Path.Combine(tuningDir, "MyBenchmark-report.md"))); + Assert.False(File.Exists(Path.Combine(tuningDir, "BenchmarkRun-20240101-120000.log"))); + + TestOutput.WriteLine("BenchmarkRun files correctly excluded from PostProcessArtifacts move."); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldSkipDuplicateAssembly_WhenSameIdentityAppearsAtTwoPaths() + { + // This test exercises lines 141-142 (the duplicate-assembly-in-queue guard): + // + // if (assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName))) + // { + // continue; // ← lines 141-142 + // } + // + // The guard fires when: + // 1. 'alreadyLoaded' (snapshot taken BEFORE the loop) does NOT contain the assembly, + // 2. the loop adds path1 of the assembly to 'assemblies', + // 3. the loop then encounters path2 with the SAME identity – + // 'alreadyLoaded' still has no entry (same snapshot), so the first `if` is skipped, + // but `assemblies.Any(...)` is TRUE → duplicate guard fires. + // + // To guarantee the assembly is never in the initial AppDomain snapshot we generate a + // brand-new assembly with a unique GUID-based name via PersistedAssemblyBuilder. + + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var buildConfig = IsDebugBuild ? "Debug" : "Release"; + try + { + // Create two subdirectories that both match the build segment + var proj1Dir = Path.Combine(tempPath, "tuning", "proj1", "bin", buildConfig, "net10.0"); + var proj2Dir = Path.Combine(tempPath, "tuning", "proj2", "bin", buildConfig, "net10.0"); + Directory.CreateDirectory(proj1Dir); + Directory.CreateDirectory(proj2Dir); + + // Build a minimal in-memory assembly with a guaranteed-unique name + var uniqueId = Guid.NewGuid().ToString("N"); + var asmName = new AssemblyName($"Unique{uniqueId}.Benchmarks") { Version = new Version(1, 0, 0, 0) }; + + var persistedBuilder = new System.Reflection.Emit.PersistedAssemblyBuilder(asmName, typeof(object).Assembly); + persistedBuilder.DefineDynamicModule("main") + .DefineType("Placeholder", System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class) + .CreateType(); + + // Save to path1, then copy to path2 so both carry the SAME assembly identity + var fileName = $"Unique{uniqueId}.Benchmarks.dll"; + var path1 = Path.Combine(proj1Dir, fileName); + var path2 = Path.Combine(proj2Dir, fileName); + persistedBuilder.Save(path1); + File.Copy(path1, path2); + + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + var workspace = new BenchmarkWorkspace(options); + + // Act – both paths match, one is loaded, the second hits the duplicate guard + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert – only ONE assembly loaded despite two matching paths + Assert.NotNull(assemblies); + Assert.Equal(1, assemblies.Length); + Assert.Contains(assemblies, a => a.GetName().Name == asmName.Name); + + TestOutput.WriteLine($"Loaded {assemblies.Length} assembly (duplicate suppressed): {assemblies[0].GetName().Name}"); + } + finally + { + // Unload assemblies and release file locks before cleanup on Windows + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + // On Windows, loaded DLLs may still hold locks; retry deletion with delay + if (Directory.Exists(tempPath)) + { + const int maxRetries = 5; + const int delayMs = 200; + var lastException = (Exception)null; + for (int i = 0; i < maxRetries; i++) + { + try + { + Directory.Delete(tempPath, true); + break; + } + catch (UnauthorizedAccessException ex) + { + lastException = ex; + if (i < maxRetries - 1) + { + System.Threading.Thread.Sleep(delayMs); + } + } + } + // If we still can't delete after retries, log but don't fail the test + // The OS will clean up the temp directory eventually + if (Directory.Exists(tempPath)) + { + TestOutput.WriteLine($"Warning: Could not delete temp directory {tempPath}"); + } + } + } + } +} diff --git a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs index ac736a1..ef6384a 100644 --- a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs +++ b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs @@ -1,4 +1,4 @@ -using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; using Codebelt.Extensions.BenchmarkDotNet.Console; diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs index 0371c76..bcb2941 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs @@ -1,72 +1,72 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using System; -using System.IO; -using System.Reflection; -using Cuemon; -using Cuemon.Reflection; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -[MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -public class BenchmarkWorkspaceBenchmark -{ - private BenchmarkWorkspace _workspace; - - [GlobalSetup] - public void GlobalSetup() - { - var isDebugBuild = Decorator.Enclose(GetType().Assembly).IsDebugBuild(); - var options = new BenchmarkWorkspaceOptions() - { - RepositoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()), - AllowDebugBuild = isDebugBuild - }; - _workspace = new BenchmarkWorkspace(options); - - var buildConfig = isDebugBuild - ? "Debug" - : "Release"; - var tuningDir = Path.Combine(options.RepositoryPath, "tuning"); - - CreateBenchmarkAssembly(tuningDir, buildConfig, "net10.0", "Valid.Benchmarks.dll"); - CreateBenchmarkAssembly(tuningDir, buildConfig, "net9.0", "Valid.Benchmarks.dll"); - } - - [Benchmark(Baseline = true, Description = "Construct BenchmarkDotNetWorkspace")] - public void ConstructWorkspaceBenchmark() - { - var v = new BenchmarkWorkspace(new BenchmarkWorkspaceOptions()); - } - - [Benchmark(Description = "Load assemblies from tuning folder (no matching assemblies)")] - public void LoadBenchmarkAssembliesBenchmark() - { - // In the prepared environment there are no matching *.Benchmarks.dll files, - // so LoadBenchmarkAssemblies will exercise the enumeration/path logic without loading assemblies. - var result = _workspace.LoadBenchmarkAssemblies(); - GC.KeepAlive(result); - } - - [Benchmark(Description = "PostProcessArtifacts (move results -> tuning folder)")] - public void PostProcessArtifactsBenchmark() - { - _workspace.PostProcessArtifacts(); - } - - /// - /// Creates a benchmark assembly by copying the executing assembly to a target framework-specific build directory. - /// - /// The base tuning directory path. - /// The build configuration (e.g., "Debug" or "Release"). - /// The target framework moniker (e.g., "net9.0", "net10.0"). - /// The name of the DLL file to create. - private void CreateBenchmarkAssembly(string tuningDir, string buildConfig, string targetFramework, string dllName) - { - var buildDir = Path.Combine(tuningDir, "bin", buildConfig, targetFramework); - Directory.CreateDirectory(buildDir); - var targetDll = Path.Combine(buildDir, dllName); - File.Copy(Assembly.GetExecutingAssembly().Location, targetDll, true); - } -} +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using System; +using System.IO; +using System.Reflection; +using Cuemon; +using Cuemon.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkWorkspaceBenchmark +{ + private BenchmarkWorkspace _workspace; + + [GlobalSetup] + public void GlobalSetup() + { + var isDebugBuild = Decorator.Enclose(GetType().Assembly).IsDebugBuild(); + var options = new BenchmarkWorkspaceOptions() + { + RepositoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()), + AllowDebugBuild = isDebugBuild + }; + _workspace = new BenchmarkWorkspace(options); + + var buildConfig = isDebugBuild + ? "Debug" + : "Release"; + var tuningDir = Path.Combine(options.RepositoryPath, "tuning"); + + CreateBenchmarkAssembly(tuningDir, buildConfig, "net10.0", "Valid.Benchmarks.dll"); + CreateBenchmarkAssembly(tuningDir, buildConfig, "net9.0", "Valid.Benchmarks.dll"); + } + + [Benchmark(Baseline = true, Description = "Construct BenchmarkDotNetWorkspace")] + public void ConstructWorkspaceBenchmark() + { + var v = new BenchmarkWorkspace(new BenchmarkWorkspaceOptions()); + } + + [Benchmark(Description = "Load assemblies from tuning folder (no matching assemblies)")] + public void LoadBenchmarkAssembliesBenchmark() + { + // In the prepared environment there are no matching *.Benchmarks.dll files, + // so LoadBenchmarkAssemblies will exercise the enumeration/path logic without loading assemblies. + var result = _workspace.LoadBenchmarkAssemblies(); + GC.KeepAlive(result); + } + + [Benchmark(Description = "PostProcessArtifacts (move results -> tuning folder)")] + public void PostProcessArtifactsBenchmark() + { + _workspace.PostProcessArtifacts(); + } + + /// + /// Creates a benchmark assembly by copying the executing assembly to a target framework-specific build directory. + /// + /// The base tuning directory path. + /// The build configuration (e.g., "Debug" or "Release"). + /// The target framework moniker (e.g., "net9.0", "net10.0"). + /// The name of the DLL file to create. + private void CreateBenchmarkAssembly(string tuningDir, string buildConfig, string targetFramework, string dllName) + { + var buildDir = Path.Combine(tuningDir, "bin", buildConfig, targetFramework); + Directory.CreateDirectory(buildDir); + var targetDll = Path.Combine(buildDir, dllName); + File.Copy(Assembly.GetExecutingAssembly().Location, targetDll, true); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs index 0006210..a2d99a6 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs @@ -1,114 +1,114 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using System; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -[MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -public class BenchmarkWorkspaceOptionsBenchmark -{ - private BenchmarkWorkspaceOptions _options; - - [GlobalSetup] - public void Setup() - { - // Pre-allocate an instance for benchmarks that need existing state - _options = new BenchmarkWorkspaceOptions(); - } - - [Benchmark(Baseline = true, Description = "Create default BenchmarkWorkspaceOptions")] - [BenchmarkCategory("Construction")] - public BenchmarkWorkspaceOptions CreateDefaultOptions() - { - return new BenchmarkWorkspaceOptions(); - } - - [Benchmark(Description = "Create and configure BenchmarkWorkspaceOptions")] - [BenchmarkCategory("Construction")] - public BenchmarkWorkspaceOptions CreateAndConfigureOptions() - { - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = AppContext.BaseDirectory, - RepositoryTuningFolder = "tuning", - RepositoryReportsFolder = "reports", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net9.0", - AllowDebugBuild = false - }; - return options; - } - - [Benchmark(Description = "ValidateOptions - valid state")] - [BenchmarkCategory("Validation")] - public void ValidateOptions_ValidState() - { - _options.ValidateOptions(); - } - - [Benchmark(Description = "PostConfigureOptions - default config")] - [BenchmarkCategory("Configuration")] - public void PostConfigureOptions_DefaultConfig() - { - var options = new BenchmarkWorkspaceOptions(); - options.PostConfigureOptions(); - GC.KeepAlive(options); - } - - [Benchmark(Description = "PostConfigureOptions - custom config")] - [BenchmarkCategory("Configuration")] - public void PostConfigureOptions_CustomConfig() - { - var options = new BenchmarkWorkspaceOptions - { - Configuration = ManualConfig.CreateEmpty() - }; - options.PostConfigureOptions(); - GC.KeepAlive(options); - } - - [Benchmark(Description = "Property access - RepositoryPath")] - [BenchmarkCategory("PropertyAccess")] - public string AccessRepositoryPath() - { - return _options.RepositoryPath; - } - - [Benchmark(Description = "Property access - Configuration")] - [BenchmarkCategory("PropertyAccess")] - public IConfig AccessConfiguration() - { - return _options.Configuration; - } - - [Benchmark(Description = "Property modification - set all properties")] - [BenchmarkCategory("PropertyModification")] - public void SetAllProperties() - { - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = AppContext.BaseDirectory, - RepositoryTuningFolder = "custom-tuning", - RepositoryReportsFolder = "custom-reports", - BenchmarkProjectSuffix = "Perf", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = true - }; - GC.KeepAlive(options); - } - - [Benchmark(Description = "Full lifecycle - create, configure, validate")] - [BenchmarkCategory("Lifecycle")] - public void FullLifecycle() - { - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = AppContext.BaseDirectory, - TargetFrameworkMoniker = "net9.0" - }; - options.PostConfigureOptions(); - options.ValidateOptions(); - GC.KeepAlive(options); - } -} +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using System; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkWorkspaceOptionsBenchmark +{ + private BenchmarkWorkspaceOptions _options; + + [GlobalSetup] + public void Setup() + { + // Pre-allocate an instance for benchmarks that need existing state + _options = new BenchmarkWorkspaceOptions(); + } + + [Benchmark(Baseline = true, Description = "Create default BenchmarkWorkspaceOptions")] + [BenchmarkCategory("Construction")] + public BenchmarkWorkspaceOptions CreateDefaultOptions() + { + return new BenchmarkWorkspaceOptions(); + } + + [Benchmark(Description = "Create and configure BenchmarkWorkspaceOptions")] + [BenchmarkCategory("Construction")] + public BenchmarkWorkspaceOptions CreateAndConfigureOptions() + { + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = AppContext.BaseDirectory, + RepositoryTuningFolder = "tuning", + RepositoryReportsFolder = "reports", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net9.0", + AllowDebugBuild = false + }; + return options; + } + + [Benchmark(Description = "ValidateOptions - valid state")] + [BenchmarkCategory("Validation")] + public void ValidateOptions_ValidState() + { + _options.ValidateOptions(); + } + + [Benchmark(Description = "PostConfigureOptions - default config")] + [BenchmarkCategory("Configuration")] + public void PostConfigureOptions_DefaultConfig() + { + var options = new BenchmarkWorkspaceOptions(); + options.PostConfigureOptions(); + GC.KeepAlive(options); + } + + [Benchmark(Description = "PostConfigureOptions - custom config")] + [BenchmarkCategory("Configuration")] + public void PostConfigureOptions_CustomConfig() + { + var options = new BenchmarkWorkspaceOptions + { + Configuration = ManualConfig.CreateEmpty() + }; + options.PostConfigureOptions(); + GC.KeepAlive(options); + } + + [Benchmark(Description = "Property access - RepositoryPath")] + [BenchmarkCategory("PropertyAccess")] + public string AccessRepositoryPath() + { + return _options.RepositoryPath; + } + + [Benchmark(Description = "Property access - Configuration")] + [BenchmarkCategory("PropertyAccess")] + public IConfig AccessConfiguration() + { + return _options.Configuration; + } + + [Benchmark(Description = "Property modification - set all properties")] + [BenchmarkCategory("PropertyModification")] + public void SetAllProperties() + { + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = AppContext.BaseDirectory, + RepositoryTuningFolder = "custom-tuning", + RepositoryReportsFolder = "custom-reports", + BenchmarkProjectSuffix = "Perf", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = true + }; + GC.KeepAlive(options); + } + + [Benchmark(Description = "Full lifecycle - create, configure, validate")] + [BenchmarkCategory("Lifecycle")] + public void FullLifecycle() + { + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = AppContext.BaseDirectory, + TargetFrameworkMoniker = "net9.0" + }; + options.PostConfigureOptions(); + options.ValidateOptions(); + GC.KeepAlive(options); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs index 06b45fc..1ff8af9 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs @@ -1,74 +1,74 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -[MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -public class BenchmarkContextBenchmark -{ - private string[] _emptyArgs; - private string[] _smallArgs; - private string[] _mediumArgs; - private string[] _largeArgs; - - [Params(0, 8, 64, 256)] - public int ArgsCount { get; set; } - - [GlobalSetup] - public void Setup() - { - // Deterministic initialization of test data - _emptyArgs = []; - - _smallArgs = new string[8]; - for (int i = 0; i < _smallArgs.Length; i++) - { - _smallArgs[i] = $"--arg{i}"; - } - - _mediumArgs = new string[64]; - for (int i = 0; i < _mediumArgs.Length; i++) - { - _mediumArgs[i] = $"--option{i}"; - } - - _largeArgs = new string[256]; - for (int i = 0; i < _largeArgs.Length; i++) - { - _largeArgs[i] = $"--parameter{i}=value{i}"; - } - } - - [Benchmark(Baseline = true, Description = "Construct BenchmarkContext with empty args")] - public BenchmarkContext ConstructWithEmptyArgs() - { - return new BenchmarkContext(_emptyArgs); - } - - [Benchmark(Description = "Construct BenchmarkContext with null args")] - public BenchmarkContext ConstructWithNullArgs() - { - return new BenchmarkContext(null); - } - - [Benchmark(Description = "Construct BenchmarkContext with varied args count")] - public BenchmarkContext ConstructWithVariedArgs() - { - return ArgsCount switch - { - 0 => new BenchmarkContext(_emptyArgs), - 8 => new BenchmarkContext(_smallArgs), - 64 => new BenchmarkContext(_mediumArgs), - 256 => new BenchmarkContext(_largeArgs), - _ => new BenchmarkContext(_emptyArgs) - }; - } - - [Benchmark(Description = "Access Args property")] - public string[] AccessArgsProperty() - { - var context = new BenchmarkContext(_smallArgs); - return context.Args; - } -} +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkContextBenchmark +{ + private string[] _emptyArgs; + private string[] _smallArgs; + private string[] _mediumArgs; + private string[] _largeArgs; + + [Params(0, 8, 64, 256)] + public int ArgsCount { get; set; } + + [GlobalSetup] + public void Setup() + { + // Deterministic initialization of test data + _emptyArgs = []; + + _smallArgs = new string[8]; + for (int i = 0; i < _smallArgs.Length; i++) + { + _smallArgs[i] = $"--arg{i}"; + } + + _mediumArgs = new string[64]; + for (int i = 0; i < _mediumArgs.Length; i++) + { + _mediumArgs[i] = $"--option{i}"; + } + + _largeArgs = new string[256]; + for (int i = 0; i < _largeArgs.Length; i++) + { + _largeArgs[i] = $"--parameter{i}=value{i}"; + } + } + + [Benchmark(Baseline = true, Description = "Construct BenchmarkContext with empty args")] + public BenchmarkContext ConstructWithEmptyArgs() + { + return new BenchmarkContext(_emptyArgs); + } + + [Benchmark(Description = "Construct BenchmarkContext with null args")] + public BenchmarkContext ConstructWithNullArgs() + { + return new BenchmarkContext(null); + } + + [Benchmark(Description = "Construct BenchmarkContext with varied args count")] + public BenchmarkContext ConstructWithVariedArgs() + { + return ArgsCount switch + { + 0 => new BenchmarkContext(_emptyArgs), + 8 => new BenchmarkContext(_smallArgs), + 64 => new BenchmarkContext(_mediumArgs), + 256 => new BenchmarkContext(_largeArgs), + _ => new BenchmarkContext(_emptyArgs) + }; + } + + [Benchmark(Description = "Access Args property")] + public string[] AccessArgsProperty() + { + var context = new BenchmarkContext(_smallArgs); + return context.Args; + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs index f127580..a5f20a3 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs @@ -1,60 +1,60 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using Cuemon; -using Cuemon.Reflection; -using System.Reflection; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -[MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -public class BenchmarkProgramBenchmark -{ - private Assembly _testAssembly; - private Assembly _entryAssembly; - - [GlobalSetup] - public void Setup() - { - // Deterministic initialization of test data - _testAssembly = Assembly.GetExecutingAssembly(); - _entryAssembly = Assembly.GetEntryAssembly() ?? _testAssembly; - } - - [Benchmark(Baseline = true, Description = "Access BuildConfiguration property")] - public string AccessBuildConfiguration() - { - return BenchmarkProgram.BuildConfiguration; - } - - [Benchmark(Description = "Access IsDebugBuild property")] - public bool AccessIsDebugBuild() - { - return BenchmarkProgram.IsDebugBuild; - } - - [Benchmark(Description = "Check assembly debug build status")] - public bool CheckAssemblyDebugBuild() - { - return Decorator.Enclose(_testAssembly).IsDebugBuild(); - } - - [Benchmark(Description = "Resolve build configuration from assembly")] - public string ResolveBuildConfiguration() - { - var isDebugBuild = Decorator.Enclose(_testAssembly).IsDebugBuild(); - return isDebugBuild ? "Debug" : "Release"; - } - - [Benchmark(Description = "Check entry assembly debug build status")] - public bool CheckEntryAssemblyDebugBuild() - { - return Decorator.Enclose(_entryAssembly).IsDebugBuild(); - } - - [Benchmark(Description = "Static property access pattern")] - public (string, bool) AccessStaticProperties() - { - return (BenchmarkProgram.BuildConfiguration, BenchmarkProgram.IsDebugBuild); - } -} +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using Cuemon; +using Cuemon.Reflection; +using System.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkProgramBenchmark +{ + private Assembly _testAssembly; + private Assembly _entryAssembly; + + [GlobalSetup] + public void Setup() + { + // Deterministic initialization of test data + _testAssembly = Assembly.GetExecutingAssembly(); + _entryAssembly = Assembly.GetEntryAssembly() ?? _testAssembly; + } + + [Benchmark(Baseline = true, Description = "Access BuildConfiguration property")] + public string AccessBuildConfiguration() + { + return BenchmarkProgram.BuildConfiguration; + } + + [Benchmark(Description = "Access IsDebugBuild property")] + public bool AccessIsDebugBuild() + { + return BenchmarkProgram.IsDebugBuild; + } + + [Benchmark(Description = "Check assembly debug build status")] + public bool CheckAssemblyDebugBuild() + { + return Decorator.Enclose(_testAssembly).IsDebugBuild(); + } + + [Benchmark(Description = "Resolve build configuration from assembly")] + public string ResolveBuildConfiguration() + { + var isDebugBuild = Decorator.Enclose(_testAssembly).IsDebugBuild(); + return isDebugBuild ? "Debug" : "Release"; + } + + [Benchmark(Description = "Check entry assembly debug build status")] + public bool CheckEntryAssemblyDebugBuild() + { + return Decorator.Enclose(_entryAssembly).IsDebugBuild(); + } + + [Benchmark(Description = "Static property access pattern")] + public (string, bool) AccessStaticProperties() + { + return (BenchmarkProgram.BuildConfiguration, BenchmarkProgram.IsDebugBuild); + } +} From 063355e436c2eb745745e5ab12d6eaef2afc04e1 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 22 Oct 2026 18:19:29 +0200 Subject: [PATCH 06/20] =?UTF-8?q?=F0=9F=94=A7=20target=20net11.0=20in=20be?= =?UTF-8?q?nchmark=20runner=20and=20tuning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The benchmark runner and tuning workspaces now run on .NET 11 and .NET 10 instead of .NET 9, so benchmark results reflect the runtimes this library supports. --- .../BenchmarkRunnerProgramTest.cs | 3 ++- tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs | 4 ++-- .../BenchmarkWorkspaceBenchmark.cs | 4 ++-- .../BenchmarkWorkspaceOptionsBenchmark.cs | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkRunnerProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkRunnerProgramTest.cs index f048084..da4deb8 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkRunnerProgramTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkRunnerProgramTest.cs @@ -42,8 +42,9 @@ public void Program_Main_ShouldConfigureBenchmarkWorkspaceOptions() Assert.Equal(BenchmarkProgram.IsDebugBuild, options.AllowDebugBuild); Assert.True(options.SkipBenchmarksWithReports); - Assert.Contains(jobs, job => job.Environment.Runtime == CoreRuntime.Core90); + Assert.DoesNotContain(jobs, job => job.Environment.Runtime == CoreRuntime.Core90); Assert.Contains(jobs, job => job.Environment.Runtime == CoreRuntime.Core10_0); + Assert.Contains(jobs, job => job.Environment.Runtime?.MsBuildMoniker == "net11.0"); } } #endif diff --git a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs index ef6384a..de62b42 100644 --- a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs +++ b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs @@ -16,8 +16,8 @@ public static void Main(string[] args) o.ConfigureBenchmarkDotNet(c => { var slimJob = BenchmarkWorkspaceOptions.Slim; - return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) - .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)) + .AddJob(slimJob.WithRuntime(CoreRuntime.CreateForNewVersion("net11.0", ".NET 11.0"))); }); }); } diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs index bcb2941..e6bc104 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs @@ -31,7 +31,7 @@ public void GlobalSetup() var tuningDir = Path.Combine(options.RepositoryPath, "tuning"); CreateBenchmarkAssembly(tuningDir, buildConfig, "net10.0", "Valid.Benchmarks.dll"); - CreateBenchmarkAssembly(tuningDir, buildConfig, "net9.0", "Valid.Benchmarks.dll"); + CreateBenchmarkAssembly(tuningDir, buildConfig, "net11.0", "Valid.Benchmarks.dll"); } [Benchmark(Baseline = true, Description = "Construct BenchmarkDotNetWorkspace")] @@ -60,7 +60,7 @@ public void PostProcessArtifactsBenchmark() /// /// The base tuning directory path. /// The build configuration (e.g., "Debug" or "Release"). - /// The target framework moniker (e.g., "net9.0", "net10.0"). + /// The target framework moniker (e.g., "net10.0", "net11.0"). /// The name of the DLL file to create. private void CreateBenchmarkAssembly(string tuningDir, string buildConfig, string targetFramework, string dllName) { diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs index a2d99a6..675506b 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs @@ -34,7 +34,7 @@ public BenchmarkWorkspaceOptions CreateAndConfigureOptions() RepositoryTuningFolder = "tuning", RepositoryReportsFolder = "reports", BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net9.0", + TargetFrameworkMoniker = "net11.0", AllowDebugBuild = false }; return options; @@ -105,7 +105,7 @@ public void FullLifecycle() var options = new BenchmarkWorkspaceOptions { RepositoryPath = AppContext.BaseDirectory, - TargetFrameworkMoniker = "net9.0" + TargetFrameworkMoniker = "net11.0" }; options.PostConfigureOptions(); options.ValidateOptions(); From 42a07d6f68b458cf6fcb04049f69f3291f2de1a4 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 22 Oct 2026 18:19:32 +0200 Subject: [PATCH 07/20] =?UTF-8?q?=F0=9F=91=B7=20default=20release=20notes?= =?UTF-8?q?=20availability=20to=20.NET=2011=20and=2010?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI release notes job falls back to .NET 11 and .NET 10 when no availability line exists, matching the supported targets. --- .github/workflows/service-update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/service-update.yml b/.github/workflows/service-update.yml index 1f69586..f55bbc4 100644 --- a/.github/workflows/service-update.yml +++ b/.github/workflows/service-update.yml @@ -125,7 +125,7 @@ jobs: NEW="${{ steps.newver.outputs.new }}" for f in .nuget/*/PackageReleaseNotes.txt; do [ -f "$f" ] || continue - TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //' || echo ".NET 10, .NET 9 and .NET Standard 2.0") + TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //' || echo ".NET 11 and .NET 10") ENTRY="Version: ${NEW}\nAvailability: ${TFM}\n\n# ALM\n- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)\n\n" { printf "$ENTRY"; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f" done From cfc6e4dd0d9ce754636166b07f64418e5e1d4eb9 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 22 Oct 2026 18:19:32 +0200 Subject: [PATCH 08/20] =?UTF-8?q?=F0=9F=93=9A=20document=20.NET=2011=20and?= =?UTF-8?q?=20.NET=2010=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README, agent guidance, contributing guide, and DocFX availability include now describe .NET 11 and .NET 10 as the supported targets. --- .docfx/includes/availability-modern.md | 2 +- .github/CONTRIBUTING.md | 2 +- AGENTS.md | 2 +- README.md | 7 ++++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.docfx/includes/availability-modern.md b/.docfx/includes/availability-modern.md index 544cb79..a13a3f6 100644 --- a/.docfx/includes/availability-modern.md +++ b/.docfx/includes/availability-modern.md @@ -1 +1 @@ -Availability: .NET 10 and .NET 9.0 \ No newline at end of file +Availability: .NET 11 and .NET 10 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9520cf3..a14d35f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -5,7 +5,7 @@ This repository is part of the Codebelt .NET library estate. The instructions be ## Before you start - Read the repository `README.md` and open an issue before starting a non-trivial feature or behavioral change. -- Use an installed .NET SDK that can build the target frameworks listed below. This repository currently targets: **net10.0, net9.0**. +- Use an installed .NET SDK that can build the target frameworks listed below. This repository currently targets: **net11.0, net10.0**. - The solution is `Codebelt.Extensions.BenchmarkDotNet.slnx`. Central package versions are maintained in `Directory.Packages.props`. - The shared build behavior is in `Directory.Build.props` and `Directory.Build.targets`; repository-specific TFMs, package references and metadata remain local to this library. diff --git a/AGENTS.md b/AGENTS.md index 0641fc1..f7a0c57 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This document provides guidance for AI agents working in this repository. ## Project Overview -Codebelt.Extensions.BenchmarkDotNet is a .NET library that provides a uniform, opinionated, and extensible way of working with BenchmarkDotNet. The solution targets .NET 10.0 and .NET 9.0. +Codebelt.Extensions.BenchmarkDotNet is a .NET library that provides a uniform, opinionated, and extensible way of working with BenchmarkDotNet. The solution targets .NET 11.0 and .NET 10.0. ## Coding Standards diff --git a/README.md b/README.md index 52585e3..47111f7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ An open-source project (MIT license) that targets and complements the [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) performance library. It provides a uniform and convenient way of doing benchmarking for all project types in .NET. -Your versatile BenchmarkDotNet companion for modern development with `.NET 9` and `.NET 10`. +Your versatile BenchmarkDotNet companion for modern development with `.NET 10` and `.NET 11`. It is, by heart, free, flexible and built to extend and boost your agile codebelt. @@ -60,6 +60,7 @@ Benchmarks are executed through a console-hosted Generic Host, allowing full par using BenchmarkDotNet.Configs; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; +using Codebelt.Extensions.BenchmarkDotNet; using Codebelt.Extensions.BenchmarkDotNet.Console; public class Program @@ -72,8 +73,8 @@ public class Program o.ConfigureBenchmarkDotNet(c => { var slimJob = BenchmarkWorkspaceOptions.Slim; - return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) - .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)) + .AddJob(slimJob.WithRuntime(CoreRuntime.CreateForNewVersion("net11.0", ".NET 11.0"))); }); }); } From a746cd0970e76df456442edc0d33547f4262f618 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Thu, 22 Oct 2026 18:19:36 +0200 Subject: [PATCH 09/20] =?UTF-8?q?=F0=9F=93=A6=20note=202.0.0=20framework?= =?UTF-8?q?=20change=20in=20package=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both packages now list .NET 11 and .NET 10 as their availability in version 2.0.0, since .NET 9 support was dropped. --- .../PackageReleaseNotes.txt | 66 ++++++++++--------- .../PackageReleaseNotes.txt | 66 ++++++++++--------- 2 files changed, 72 insertions(+), 60 deletions(-) diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt index 3622691..02121e0 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt @@ -1,33 +1,39 @@ -Version: 1.3.5 -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.3.4 -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.3.3 -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.3.2 -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.3.1 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - +Version: 2.0.0 +Availability: .NET 11 and .NET 10 + +# ALM +- CHANGED Target frameworks by adding .NET 11 (net11.0) and removing .NET 9 (net9.0), so the package now targets .NET 11 and .NET 10 + +Version: 1.3.5 +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.3.4 +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.3.3 +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.3.2 +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.3.1 +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.3.0 Availability: .NET 10 and .NET 9 diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt index 42d7785..01db84b 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt @@ -1,33 +1,39 @@ -Version: 1.3.5 -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.3.4 -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.3.3 -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.3.2 -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.3.1 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - +Version: 2.0.0 +Availability: .NET 11 and .NET 10 + +# ALM +- CHANGED Target frameworks by adding .NET 11 (net11.0) and removing .NET 9 (net9.0), so the package now targets .NET 11 and .NET 10 + +Version: 1.3.5 +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.3.4 +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.3.3 +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.3.2 +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.3.1 +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.3.0 Availability: .NET 10 and .NET 9 From 5cfb096dee8c2c9369e8651ac0d937d4df7ce8ff Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 22 Oct 2026 18:25:09 +0200 Subject: [PATCH 10/20] =?UTF-8?q?=F0=9F=91=B7=20enable=20preview=20builds?= =?UTF-8?q?=20in=20ci=20pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opt the reusable build, pack, test, SonarCloud, and CodeQL workflows into preview inputs so the pipeline exercises preview toolchains alongside the stable configuration. --- .github/workflows/ci-pipeline.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 37b90a2..701a3db 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -59,6 +59,7 @@ jobs: configuration: [Debug, Release] uses: codebeltnet/jobs-dotnet-build/.github/workflows/default.yml@v3 with: + include-preview: true configuration: ${{ matrix.configuration }} strong-name-key-filename: ${{ needs.init.outputs.strong-name-key-filename }} build-switches: ${{ needs.init.outputs.build-switches }} @@ -74,6 +75,7 @@ jobs: configuration: [Debug, Release] uses: codebeltnet/jobs-dotnet-pack/.github/workflows/default.yml@v3 with: + include-preview: true configuration: ${{ matrix.configuration }} version: ${{ needs.build.outputs.version }} download-build-artifact-pattern: build-${{ matrix.configuration }}-X64 @@ -88,6 +90,7 @@ jobs: arch: [X64, ARM64] uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: + include-preview: true runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} configuration: ${{ matrix.configuration }} build-switches: -p:SkipSignAssembly=true @@ -105,6 +108,7 @@ jobs: arch: [X64, ARM64] uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: + include-preview: true runs-on: ${{ matrix.arch == 'ARM64' && 'windows-11-arm' || 'windows-2025' }} configuration: ${{ matrix.configuration }} build-switches: -p:SkipSignAssembly=true @@ -123,6 +127,7 @@ jobs: configuration: [Debug, Release] uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: + include-preview: true runs-on: ${{ matrix.arch == 'ARM64' && 'macos-26' || 'macos-26-intel' }} configuration: ${{ matrix.configuration }} build-switches: -p:SkipSignAssembly=true @@ -180,6 +185,7 @@ jobs: needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 with: + include-preview: true organization: geekle projectKey: benchmarkdotnet version: ${{ needs.build.outputs.version }} @@ -199,6 +205,8 @@ jobs: name: call-codeql needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3 + with: + include-preview: true permissions: security-events: write From 5e2dfb9768fefdba6e17add8c294d8e4fe1f6372 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Thu, 22 Oct 2026 19:24:02 +0200 Subject: [PATCH 11/20] =?UTF-8?q?=F0=9F=93=9D=20align=20package=20readmes?= =?UTF-8?q?=20with=20current=20target=20frameworks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep package documentation consistent with the net11.0 and net10.0 targets by replacing stale .NET 9 references and matching the console example to the current benchmark runner. --- .../Codebelt.Extensions.BenchmarkDotNet.Console/README.md | 6 ++++-- .nuget/Codebelt.Extensions.BenchmarkDotNet/README.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md index 5cc9063..b153ea5 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md @@ -12,6 +12,8 @@ By embracing `Microsoft.Extensions.Hosting`, this package enables clean separati At its core, the package favors convention over configuration and promotes benchmarks as first-class, host-managed workloads rather than ad-hoc console routines. +Targets `.NET 11` (`net11.0`) and `.NET 10` (`net10.0`). + ## CSharp Example Benchmarks are executed through a console-hosted Generic Host, allowing full participation in dependency injection, configuration, logging and more. @@ -32,8 +34,8 @@ public class Program o.ConfigureBenchmarkDotNet(c => { var slimJob = BenchmarkWorkspaceOptions.Slim; - return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) - .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)) + .AddJob(slimJob.WithRuntime(CoreRuntime.CreateForNewVersion("net11.0", ".NET 11.0"))); }); }); } diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md b/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md index 88556e8..fcd0ea8 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md @@ -6,7 +6,7 @@ A unified, opinionated foundation for building robust BenchmarkDotNet workflows **Codebelt.Extensions.BenchmarkDotNet** is part of a modern, MIT-licensed ecosystem designed to bring clarity, structure, and consistency to BenchmarkDotNet projects. -If you value predictable conventions, clean separation of responsibilities, and benchmarks that scale gracefully across `.NET 9` and `.NET 10`, this library is your agile companion. +If you value predictable conventions, clean separation of responsibilities, and benchmarks that scale gracefully across `.NET 11` (`net11.0`) and `.NET 10` (`net10.0`), this library is your agile companion. It removes unnecessary ceremony while embracing best practices from other consumers of BenchmarkDotNet, so you can focus on performance insights, not plumbing. From c95c59ce86436a66bbade1d558b44396a25c0ced Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 3 Nov 2026 20:09:46 +0100 Subject: [PATCH 12/20] =?UTF-8?q?=F0=9F=94=A5=20remove=20benchmark=20promp?= =?UTF-8?q?t=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The benchmark prompt template is no longer needed by the repository, so it is removed to keep agent guidance focused on the maintained instructions. --- .github/prompts/benchmark.prompt.md | 164 ---------------------------- 1 file changed, 164 deletions(-) delete mode 100644 .github/prompts/benchmark.prompt.md diff --git a/.github/prompts/benchmark.prompt.md b/.github/prompts/benchmark.prompt.md deleted file mode 100644 index b37a123..0000000 --- a/.github/prompts/benchmark.prompt.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -mode: agent -description: 'Writing Performance Benchmarks in Codebelt.Extensions.BenchmarkDotNet' ---- - -# Benchmark Fixture Prompt (Codebelt.Extensions.BenchmarkDotNet Tuning Benchmarks) - -This prompt defines how to generate performance tests (“benchmarks”) for the Codebelt.Extensions.BenchmarkDotNet codebase using BenchmarkDotNet. -Benchmarks in Codebelt.Extensions.BenchmarkDotNet are *not* unit tests — they are micro- or component-level performance measurements that belong under the `tuning/` directory and follow strict conventions. - -Copilot must follow these guidelines when generating benchmark fixtures. - ---- - -## 1. Naming and Placement - -- All benchmark projects live under the `tuning/` folder. - Examples: - - `tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/` - - `tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/` - -- **Namespaces must NOT end with `.Benchmarks`.** - They must mirror the production assembly’s namespace. - - Example: - If benchmarking a type inside `Codebelt.Extensions.BenchmarkDotNet.Console`, then: - - ```csharp - namespace Codebelt.Extensions.BenchmarkDotNet.Console - { - public class Sha512256Benchmark { … } - } - ``` - -* **Benchmark class names must end with `Benchmark`.** - Example: `DateSpanBenchmark`, `FowlerNollVoBenchmark`. - -* Benchmark files should be located in the matching benchmark project - (e.g., benchmarks for `Codebelt.Extensions.BenchmarkDotNet.Console` go in `Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj`). - -* In the `.csproj` for each benchmark project, set the root namespace to the production namespace: - - ```xml - Codebelt.Extensions.BenchmarkDotNet.Console - ``` - ---- - -## 2. Attributes and Configuration - -Each benchmark class should use: - -```csharp -[MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -``` - -Optional but strongly recommended where meaningful: - -* `[Params(...)]` — define small, medium, large input sizes. -* `[GlobalSetup]` — deterministic initialization of benchmark data. -* `[Benchmark(Description = "...")]` — always add descriptions. -* `[Benchmark(Baseline = true)]` — when comparing two implementations. - -Avoid complex global configs; prefer explicit attributes inside the class. - ---- - -## 3. Structure and Best Practices - -A benchmark fixture must: - -* Measure a **single logical operation** per benchmark method. -* Avoid I/O, networking, disk access, logging, or side effects. -* Avoid expensive setup inside `[Benchmark]` methods. -* Use deterministic data (e.g., seeded RNG or predefined constants). -* Use `[GlobalSetup]` to allocate buffers, random payloads, or reusable test data only once. -* Avoid shared mutable state unless reset per iteration. - -Use representative input sizes such as: - -```csharp -[Params(8, 256, 4096)] -public int Count { get; set; } -``` - -BenchmarkDotNet will run each benchmark for each parameter value. - ---- - -## 4. Method Naming Conventions - -Use descriptive names that communicate intent: - -* `Parse_Short` -* `Parse_Long` -* `ComputeHash_Small` -* `ComputeHash_Large` -* `Serialize_Optimized` -* `Serialize_Baseline` - -When comparing approaches, always list them clearly and tag one as the baseline. - ---- - -## 5. Example Benchmark Fixture - -```csharp -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; - -namespace Codebelt.Extensions.BenchmarkDotNet -{ - [MemoryDiagnoser] - [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] - public class SampleOperationBenchmark - { - [Params(8, 256, 4096)] - public int Count { get; set; } - - private byte[] _payload; - - [GlobalSetup] - public void Setup() - { - _payload = new byte[Count]; - // deterministic initialization - } - - [Benchmark(Baseline = true, Description = "Operation - baseline")] - public int Operation_Baseline() => SampleOperation.Process(_payload); - - [Benchmark(Description = "Operation - optimized")] - public int Operation_Optimized() => SampleOperation.ProcessOptimized(_payload); - } -} -``` - ---- - -## 6. Reporting and CI - -* Benchmark projects live exclusively under `tuning/`. They must not affect production builds. -* Heavy BenchmarkDotNet runs should *not* run in CI unless explicitly configured. -* Reports are produced by the Codebelt.Extensions.BenchmarkDotNet benchmark runner and stored under its configured artifacts directory. - ---- - -## 7. Additional Guidelines - -* Keep benchmark fixtures focused and readable. -* Document non-obvious reasoning in short comments. -* Prefer realistic but deterministic data sets. -* When benchmarks reveal regressions or improvements, reference the associated PR or issue in a comment. -* Shared benchmark helpers belong in `tuning/` projects, not in production code. - ---- - -## Final Notes - -* Benchmarks are performance tests, not unit tests. -* Use `[Benchmark]` only for pure performance measurement. -* Avoid `MethodImplOptions.NoInlining` unless absolutely necessary. -* Use small sets of meaningful benchmark scenarios — avoid combinatorial explosion. From 22e4321ffd0ace69f8b1208320372d332c5f2a64 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 3 Nov 2026 23:03:03 +0100 Subject: [PATCH 13/20] =?UTF-8?q?=F0=9F=8E=A8=20normalize=20line=20endings?= =?UTF-8?q?=20and=20drop=20byte=20order=20marks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Files are now stored with LF endings and without UTF-8 byte order marks, which keeps diffs readable and matches the text=auto eol=lf rule in .gitattributes. These files have no content changes. --- .docfx/BuildDocfxImage.ps1 | 8 +- .docfx/Dockerfile.docfx | 32 +-- .docfx/PublishDocfxImage.ps1 | 6 +- ...belt.Extensions.BenchmarkDotNet.Console.md | 2 +- .../Codebelt.Extensions.BenchmarkDotNet.md | 2 +- .docfx/docfx.json | 190 +++++++++--------- .docfx/index.md | 22 +- .docfx/packages/index.md | 24 +-- .docfx/toc.yml | 8 +- .gitattributes | 8 +- .github/dependabot.yml | 34 ++-- .../README.md | 96 ++++----- Codebelt.Extensions.BenchmarkDotNet.slnx | 38 ++-- Directory.Build.targets | 36 ++-- ....Extensions.BenchmarkDotNet.Console.csproj | 32 +-- ...Codebelt.Extensions.BenchmarkDotNet.csproj | 26 +-- ...lt.Extensions.BenchmarkDotNet.Tests.csproj | 22 +- ...t.Extensions.BenchmarkDotNet.Runner.csproj | 50 ++--- ...tensions.BenchmarkDotNet.Benchmarks.csproj | 22 +- ....BenchmarkDotNet.Console.Benchmarks.csproj | 22 +- 20 files changed, 340 insertions(+), 340 deletions(-) diff --git a/.docfx/BuildDocfxImage.ps1 b/.docfx/BuildDocfxImage.ps1 index 63fd259..911c014 100644 --- a/.docfx/BuildDocfxImage.ps1 +++ b/.docfx/BuildDocfxImage.ps1 @@ -1,4 +1,4 @@ -$version = minver -i -t v -v w -docfx metadata docfx.json -docker buildx build -t benchmarkdotnet-docfx:$version --platform linux/arm64,linux/amd64 --load -f Dockerfile.docfx . -get-childItem -recurse -path api -include *.yml, .manifest | remove-item +$version = minver -i -t v -v w +docfx metadata docfx.json +docker buildx build -t benchmarkdotnet-docfx:$version --platform linux/arm64,linux/amd64 --load -f Dockerfile.docfx . +get-childItem -recurse -path api -include *.yml, .manifest | remove-item diff --git a/.docfx/Dockerfile.docfx b/.docfx/Dockerfile.docfx index 39006f1..e29eda1 100644 --- a/.docfx/Dockerfile.docfx +++ b/.docfx/Dockerfile.docfx @@ -1,16 +1,16 @@ -ARG NGINX_VERSION=1.31-alpine - -FROM --platform=$BUILDPLATFORM dhi.io/nginx:${NGINX_VERSION} AS base -RUN rm -rf /usr/share/nginx/html/* - -FROM --platform=$BUILDPLATFORM codebeltnet/docfx:2 AS build - -ADD [".", "docfx"] - -RUN cd docfx; \ - docfx build - -FROM dhi.io/nginx:${NGINX_VERSION} AS final -WORKDIR /usr/share/nginx/html -COPY --from=build /build/docfx/wwwroot /usr/share/nginx/html -COPY nginx.conf /etc/nginx/conf.d/default.conf +ARG NGINX_VERSION=1.31-alpine + +FROM --platform=$BUILDPLATFORM dhi.io/nginx:${NGINX_VERSION} AS base +RUN rm -rf /usr/share/nginx/html/* + +FROM --platform=$BUILDPLATFORM codebeltnet/docfx:2 AS build + +ADD [".", "docfx"] + +RUN cd docfx; \ + docfx build + +FROM dhi.io/nginx:${NGINX_VERSION} AS final +WORKDIR /usr/share/nginx/html +COPY --from=build /build/docfx/wwwroot /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/.docfx/PublishDocfxImage.ps1 b/.docfx/PublishDocfxImage.ps1 index 267de57..710049d 100644 --- a/.docfx/PublishDocfxImage.ps1 +++ b/.docfx/PublishDocfxImage.ps1 @@ -1,3 +1,3 @@ -$version = minver -i -t v -v w -docker tag benchmarkdotnet-docfx:$version jcr.codebelt.net/geekle/benchmarkdotnet-docfx:$version -docker push jcr.codebelt.net/geekle/benchmarkdotnet-docfx:$version +$version = minver -i -t v -v w +docker tag benchmarkdotnet-docfx:$version jcr.codebelt.net/geekle/benchmarkdotnet-docfx:$version +docker push jcr.codebelt.net/geekle/benchmarkdotnet-docfx:$version diff --git a/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md index d95c029..a4f89fa 100644 --- a/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md +++ b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md @@ -1,4 +1,4 @@ ---- +--- uid: Codebelt.Extensions.BenchmarkDotNet.Console summary: *content --- diff --git a/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md index a71afc1..781d9b9 100644 --- a/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md +++ b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md @@ -1,4 +1,4 @@ ---- +--- uid: Codebelt.Extensions.BenchmarkDotNet summary: *content --- diff --git a/.docfx/docfx.json b/.docfx/docfx.json index 2bf81aa..62d62f0 100644 --- a/.docfx/docfx.json +++ b/.docfx/docfx.json @@ -1,95 +1,95 @@ -{ - "metadata": [ - { - "src": [ - { - "files": [ - "Codebelt.Extensions.BenchmarkDotNet/**.csproj", - "Codebelt.Extensions.BenchmarkDotNet.Console/**.csproj" - ], - "src": "../src" - } - ], - "dest": "api", - "memberLayout": "separatePages", - "filter": "filterConfig.yml", - "properties": { - "TargetFramework": "net10.0" - } - } - ], - "build": { - "xref": [ - "https://docs.cuemon.net/xrefmap.yml?v=20260120", - "https://bootstrapper.codebelt.net/xrefmap.yml", - "https://github.com/dotnet/docfx/raw/main/.xrefmap.json" - ], - "content": [ - { - "files": [ - "api/**/*.yml", - "packages/**/*.md", - "toc.yml", - "*.md" - ], - "exclude": [ - "api/namespaces/**", - "api/types/**", - "bin/**", - "obj/**" - ] - } - ], - "resource": [ - { - "files": [ - "images/**" - ] - } - ], - "globalMetadata": { - "_appTitle": "Extensions for BenchmarkDotNet by Codebelt", - "_appFooter": "Generated by DocFX. Copyright 2025-2026 Geekle. All rights reserved.", - "_appLogoPath": "images/50x50.png", - "_appFaviconPath": "images/favicon.ico", - "_googleAnalyticsTagId": "G-K2NG2TXDWQ", - "_enableSearch": false, - "_disableContribution": false, - "_gitContribute": { - "repo": "https://github.com/codebeltnet/benchmarkdotnet", - "branch": "main" - }, - "_gitUrlPattern": "github" - }, - "dest": "wwwroot", - "globalMetadataFiles": [], - "fileMetadataFiles": [], - "template": [ - "default", - "modern", - "templates/ms-style" - ], - "overwrite": [ - { - "files": [ - "api/namespaces/**/*.md", - "api/types/**/*.md" - ], - "exclude": [ - "obj/**", - "wwwroot/**" - ] - } - ], - "postProcessors": [], - "markdownEngineName": "markdig", - "noLangKeyword": false, - "keepFileLink": false, - "cleanupCacheHistory": false, - "disableGitFeatures": false, - "sitemap": { - "baseUrl": "https://benchmarkdotnet.codebelt.net/", - "changefreq": "monthly" - } - } -} +{ + "metadata": [ + { + "src": [ + { + "files": [ + "Codebelt.Extensions.BenchmarkDotNet/**.csproj", + "Codebelt.Extensions.BenchmarkDotNet.Console/**.csproj" + ], + "src": "../src" + } + ], + "dest": "api", + "memberLayout": "separatePages", + "filter": "filterConfig.yml", + "properties": { + "TargetFramework": "net10.0" + } + } + ], + "build": { + "xref": [ + "https://docs.cuemon.net/xrefmap.yml?v=20260120", + "https://bootstrapper.codebelt.net/xrefmap.yml", + "https://github.com/dotnet/docfx/raw/main/.xrefmap.json" + ], + "content": [ + { + "files": [ + "api/**/*.yml", + "packages/**/*.md", + "toc.yml", + "*.md" + ], + "exclude": [ + "api/namespaces/**", + "api/types/**", + "bin/**", + "obj/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "globalMetadata": { + "_appTitle": "Extensions for BenchmarkDotNet by Codebelt", + "_appFooter": "Generated by DocFX. Copyright 2025-2026 Geekle. All rights reserved.", + "_appLogoPath": "images/50x50.png", + "_appFaviconPath": "images/favicon.ico", + "_googleAnalyticsTagId": "G-K2NG2TXDWQ", + "_enableSearch": false, + "_disableContribution": false, + "_gitContribute": { + "repo": "https://github.com/codebeltnet/benchmarkdotnet", + "branch": "main" + }, + "_gitUrlPattern": "github" + }, + "dest": "wwwroot", + "globalMetadataFiles": [], + "fileMetadataFiles": [], + "template": [ + "default", + "modern", + "templates/ms-style" + ], + "overwrite": [ + { + "files": [ + "api/namespaces/**/*.md", + "api/types/**/*.md" + ], + "exclude": [ + "obj/**", + "wwwroot/**" + ] + } + ], + "postProcessors": [], + "markdownEngineName": "markdig", + "noLangKeyword": false, + "keepFileLink": false, + "cleanupCacheHistory": false, + "disableGitFeatures": false, + "sitemap": { + "baseUrl": "https://benchmarkdotnet.codebelt.net/", + "changefreq": "monthly" + } + } +} diff --git a/.docfx/index.md b/.docfx/index.md index 4c9e169..b41dd1b 100644 --- a/.docfx/index.md +++ b/.docfx/index.md @@ -1,11 +1,11 @@ ---- -uid: frontpage-md -title: Extensions for BenchmarkDotNet by Codebelt ---- -![Extensions for BenchmarkDotNet API by Codebelt](/images/128x128.png) - -# Extensions for BenchmarkDotNet by Codebelt - -This project is part of [Extensions for BenchmarkDotNet by Codebelt](https://github.com/codebeltnet/benchmarkdotnet). - -Proceed to the [docs](/api/Codebelt.Extensions.BenchmarkDotNet.html) to learn more about the capabilities of this project. +--- +uid: frontpage-md +title: Extensions for BenchmarkDotNet by Codebelt +--- +![Extensions for BenchmarkDotNet API by Codebelt](/images/128x128.png) + +# Extensions for BenchmarkDotNet by Codebelt + +This project is part of [Extensions for BenchmarkDotNet by Codebelt](https://github.com/codebeltnet/benchmarkdotnet). + +Proceed to the [docs](/api/Codebelt.Extensions.BenchmarkDotNet.html) to learn more about the capabilities of this project. diff --git a/.docfx/packages/index.md b/.docfx/packages/index.md index 9802b81..443df2a 100644 --- a/.docfx/packages/index.md +++ b/.docfx/packages/index.md @@ -1,12 +1,12 @@ -# NuGet Packages - -This is a list of all NuGet packages from **Extensions for BenchmarkDotNet by Codebelt** that is publicly available on [NuGet.org](https://www.nuget.org/packages?q=codebelt.extensions.benchmarkdotnet); the packages here are listed alphabetically and are available in preview-, rc- and production-ready versions. - -## 📦 Standalone Packages - -Provides a focused API for BenchmarkDotNet projects. - -|Package|vNext|Stable|Downloads| -|:--|:-:|:-:|:-:| -| [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.BenchmarkDotNet?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.BenchmarkDotNet?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.BenchmarkDotNet?color=blueviolet&logo=nuget) | -| [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.BenchmarkDotNet.Console?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.BenchmarkDotNet.Console?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.BenchmarkDotNet.Console?color=blueviolet&logo=nuget) | +# NuGet Packages + +This is a list of all NuGet packages from **Extensions for BenchmarkDotNet by Codebelt** that is publicly available on [NuGet.org](https://www.nuget.org/packages?q=codebelt.extensions.benchmarkdotnet); the packages here are listed alphabetically and are available in preview-, rc- and production-ready versions. + +## 📦 Standalone Packages + +Provides a focused API for BenchmarkDotNet projects. + +|Package|vNext|Stable|Downloads| +|:--|:-:|:-:|:-:| +| [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.BenchmarkDotNet?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.BenchmarkDotNet?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.BenchmarkDotNet?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.BenchmarkDotNet.Console?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.BenchmarkDotNet.Console?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.BenchmarkDotNet.Console?color=blueviolet&logo=nuget) | diff --git a/.docfx/toc.yml b/.docfx/toc.yml index 921bcba..9465545 100644 --- a/.docfx/toc.yml +++ b/.docfx/toc.yml @@ -1,4 +1,4 @@ -- name: BenchmarkDotNet API - href: api/Codebelt.Extensions.BenchmarkDotNet.html -- name: NuGet - href: packages +- name: BenchmarkDotNet API + href: api/Codebelt.Extensions.BenchmarkDotNet.html +- name: NuGet + href: packages diff --git a/.gitattributes b/.gitattributes index 1ff0c42..0ea8d1b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,7 @@ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### -* text=auto +* text=auto eol=lf ############################################################################### # Set default behavior for command prompt diff. @@ -17,7 +17,7 @@ # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following +# the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below @@ -46,9 +46,9 @@ ############################################################################### # diff behavior for common document formats -# +# # Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the +# is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1fca42a..7fe3850 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,17 +1,17 @@ -version: 2 -updates: - - package-ecosystem: "nuget" - directory: "/src" - schedule: - interval: "weekly" - open-pull-requests-limit: 0 - - package-ecosystem: "nuget" - directory: "/test" - schedule: - interval: "weekly" - open-pull-requests-limit: 0 - - package-ecosystem: "github-actions" - directory: "/.github/workflows" - schedule: - interval: "weekly" - open-pull-requests-limit: 0 +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/src" + schedule: + interval: "weekly" + open-pull-requests-limit: 0 + - package-ecosystem: "nuget" + directory: "/test" + schedule: + interval: "weekly" + open-pull-requests-limit: 0 + - package-ecosystem: "github-actions" + directory: "/.github/workflows" + schedule: + interval: "weekly" + open-pull-requests-limit: 0 diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md index b153ea5..68b85bf 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md @@ -1,48 +1,48 @@ -# Codebelt.Extensions.BenchmarkDotNet.Console - -A structured, host-based execution model for running BenchmarkDotNet benchmarks in console applications. - -## About - -**Codebelt.Extensions.BenchmarkDotNet.Console** extends the **Codebelt.Extensions.BenchmarkDotNet** package with a dedicated console runner built on the Microsoft Generic Host. - -It provides a predictable startup model, consistent dependency injection, and a managed application lifecycle for benchmark execution, aligning benchmarks with the same hosting principles used in modern .NET applications. - -By embracing `Microsoft.Extensions.Hosting`, this package enables clean separation between benchmark definition, configuration, and execution - making benchmarks easier to compose, test, and evolve over time. - -At its core, the package favors convention over configuration and promotes benchmarks as first-class, host-managed workloads rather than ad-hoc console routines. - -Targets `.NET 11` (`net11.0`) and `.NET 10` (`net10.0`). - -## CSharp Example - -Benchmarks are executed through a console-hosted Generic Host, allowing full participation in dependency injection, configuration, logging and more. - -```csharp -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Jobs; -using Codebelt.Extensions.BenchmarkDotNet.Console; - -public class Program -{ - public static void Main(string[] args) - { - BenchmarkProgram.Run(args, o => - { - o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; - o.ConfigureBenchmarkDotNet(c => - { - var slimJob = BenchmarkWorkspaceOptions.Slim; - return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)) - .AddJob(slimJob.WithRuntime(CoreRuntime.CreateForNewVersion("net11.0", ".NET 11.0"))); - }); - }); - } -} -``` - -## Related Packages - -* [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) 📦 -* [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) 📦 +# Codebelt.Extensions.BenchmarkDotNet.Console + +A structured, host-based execution model for running BenchmarkDotNet benchmarks in console applications. + +## About + +**Codebelt.Extensions.BenchmarkDotNet.Console** extends the **Codebelt.Extensions.BenchmarkDotNet** package with a dedicated console runner built on the Microsoft Generic Host. + +It provides a predictable startup model, consistent dependency injection, and a managed application lifecycle for benchmark execution, aligning benchmarks with the same hosting principles used in modern .NET applications. + +By embracing `Microsoft.Extensions.Hosting`, this package enables clean separation between benchmark definition, configuration, and execution - making benchmarks easier to compose, test, and evolve over time. + +At its core, the package favors convention over configuration and promotes benchmarks as first-class, host-managed workloads rather than ad-hoc console routines. + +Targets `.NET 11` (`net11.0`) and `.NET 10` (`net10.0`). + +## CSharp Example + +Benchmarks are executed through a console-hosted Generic Host, allowing full participation in dependency injection, configuration, logging and more. + +```csharp +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using Codebelt.Extensions.BenchmarkDotNet.Console; + +public class Program +{ + public static void Main(string[] args) + { + BenchmarkProgram.Run(args, o => + { + o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; + o.ConfigureBenchmarkDotNet(c => + { + var slimJob = BenchmarkWorkspaceOptions.Slim; + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)) + .AddJob(slimJob.WithRuntime(CoreRuntime.CreateForNewVersion("net11.0", ".NET 11.0"))); + }); + }); + } +} +``` + +## Related Packages + +* [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) 📦 +* [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) 📦 diff --git a/Codebelt.Extensions.BenchmarkDotNet.slnx b/Codebelt.Extensions.BenchmarkDotNet.slnx index 3486db2..3b99c3c 100644 --- a/Codebelt.Extensions.BenchmarkDotNet.slnx +++ b/Codebelt.Extensions.BenchmarkDotNet.slnx @@ -1,19 +1,19 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets index a5dbead..11142c1 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,18 +1,18 @@ - - - - $([System.IO.File]::ReadAllText('$(PackageReleaseNotesFile)')) - - - - - - 0 - $(MinVerMajor).$(MinVerMinor).$(MinVerPatch).$(GITHUB_RUN_NUMBER) - - - - - - - + + + + $([System.IO.File]::ReadAllText('$(PackageReleaseNotesFile)')) + + + + + + 0 + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch).$(GITHUB_RUN_NUMBER) + + + + + + + diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj b/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj index 7e8e79f..9c89437 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj @@ -1,16 +1,16 @@ - - - - The Codebelt.Extensions.BenchmarkDotNet.Console namespace contains types that provide a structured and opinionated console-hosted execution model for BenchmarkDotNet. - benchmark benchmarkdotnet console hosting generic-host dependency-injection performance - - - - - - - - - - - + + + + The Codebelt.Extensions.BenchmarkDotNet.Console namespace contains types that provide a structured and opinionated console-hosted execution model for BenchmarkDotNet. + benchmark benchmarkdotnet console hosting generic-host dependency-injection performance + + + + + + + + + + + diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj b/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj index 87bb561..4b55bc3 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj +++ b/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj @@ -1,13 +1,13 @@ - - - - The Codebelt.Extensions.BenchmarkDotNet namespace contains types that provide a uniform, opinionated, and extensible way of working with BenchmarkDotNet. - benchmark benchmarkdotnet performance microbenchmark profiling diagnostics - - - - - - - - + + + + The Codebelt.Extensions.BenchmarkDotNet namespace contains types that provide a uniform, opinionated, and extensible way of working with BenchmarkDotNet. + benchmark benchmarkdotnet performance microbenchmark profiling diagnostics + + + + + + + + diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj index 24cde66..83e3ae1 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj @@ -1,11 +1,11 @@ - - - - Codebelt.Extensions.BenchmarkDotNet - - - - - - - + + + + Codebelt.Extensions.BenchmarkDotNet + + + + + + + diff --git a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj index 4f5d4af..6b58c6d 100644 --- a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj +++ b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj @@ -1,25 +1,25 @@ - - - - Exe - net10.0 - dotnet-bdn-runner - $(MSBuildProjectName) - - - - - - - - - false - - - - - - - - - + + + + Exe + net10.0 + dotnet-bdn-runner + $(MSBuildProjectName) + + + + + + + + + false + + + + + + + + + diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj index 24cde66..83e3ae1 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj @@ -1,11 +1,11 @@ - - - - Codebelt.Extensions.BenchmarkDotNet - - - - - - - + + + + Codebelt.Extensions.BenchmarkDotNet + + + + + + + diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj index ec5743f..7909e84 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj @@ -1,11 +1,11 @@ - - - - Codebelt.Extensions.BenchmarkDotNet.Console - - - - - - - + + + + Codebelt.Extensions.BenchmarkDotNet.Console + + + + + + + From 4aca32e8f7ed1818841eca3d5a1b22f5155eab0f Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 3 Nov 2026 23:03:04 +0100 Subject: [PATCH 14/20] =?UTF-8?q?=F0=9F=94=A7=20move=20ci=20runners=20and?= =?UTF-8?q?=20test=20environments=20to=20ubuntu=2026.04?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI jobs and the test environment matrix now target Ubuntu 26.04 and split the Docker test runner into net10.0 and net11.0 environments, so each target framework is tested on its own image. --- .github/workflows/ci-pipeline.yml | 8 +++---- testenvironments.json | 35 ++++++++++++++++++------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 701a3db..6335d7a 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -23,7 +23,7 @@ permissions: jobs: init: name: initialize - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: run-mac-tests: ${{ steps.vars.outputs.run-mac-tests }} run-privileged-jobs: ${{ steps.vars.outputs.run-privileged-jobs }} @@ -63,7 +63,7 @@ jobs: configuration: ${{ matrix.configuration }} strong-name-key-filename: ${{ needs.init.outputs.strong-name-key-filename }} build-switches: ${{ needs.init.outputs.build-switches }} - runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-26.04-arm' || 'ubuntu-26.04' }} upload-build-artifact-name: build-${{ matrix.configuration }}-${{ matrix.arch }} secrets: inherit @@ -91,7 +91,7 @@ jobs: uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: include-preview: true - runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-26.04-arm' || 'ubuntu-26.04' }} configuration: ${{ matrix.configuration }} build-switches: -p:SkipSignAssembly=true build: true # we need to build due to xUnitv3 @@ -139,7 +139,7 @@ jobs: if: ${{ always() }} name: test-qualitygate needs: [init, test_linux, test_windows, test_mac] - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: Evaluate test results shell: bash diff --git a/testenvironments.json b/testenvironments.json index 587b2ef..0651f28 100644 --- a/testenvironments.json +++ b/testenvironments.json @@ -1,15 +1,20 @@ -{ - "version": "1", - "environments": [ - { - "name": "WSL-Ubuntu", - "type": "wsl", - "wslDistribution": "Ubuntu-24.04" - }, - { - "name": "Docker-Ubuntu", - "type": "docker", - "dockerImage": "codebeltnet/ubuntu-testrunner:8-9-10-11" - } - ] -} +{ + "version": "1", + "environments": [ + { + "name": "WSL-Ubuntu", + "type": "wsl", + "wslDistribution": "Ubuntu-26.04" + }, + { + "name": "Docker-Ubuntu (net10.0)", + "type": "docker", + "dockerImage": "codebeltnet/ubuntu-testrunner:10" + }, + { + "name": "Docker-Ubuntu (net11.0)", + "type": "docker", + "dockerImage": "codebeltnet/ubuntu-testrunner:11" + } + ] +} From e781d161be6d9244deb72b29fb5940f64ceac4e1 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 3 Nov 2026 23:03:04 +0100 Subject: [PATCH 15/20] =?UTF-8?q?=F0=9F=94=A7=20automate=20pending=20chang?= =?UTF-8?q?elog=20version=20handling=20in=20service=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The service update workflow now accepts a pending changelog version newer than the latest tag and validates release dates, so a planned major release is not overwritten by the next patch bump. --- .github/workflows/service-update.yml | 37 ++++++++++++++++++---------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/.github/workflows/service-update.yml b/.github/workflows/service-update.yml index f55bbc4..1f855d8 100644 --- a/.github/workflows/service-update.yml +++ b/.github/workflows/service-update.yml @@ -50,6 +50,7 @@ jobs: import re import subprocess import sys + from datetime import date tag_pattern = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") tags = subprocess.check_output(["git", "tag", "--list"], text=True).splitlines() @@ -72,28 +73,37 @@ jobs: with open("CHANGELOG.md", encoding="utf-8") as changelog_file: changelog = changelog_file.read() - heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - ([^\r\n]+)[ \t]*$", changelog) + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\]([^\r\n]*)$", changelog) if not heading: print("::error::CHANGELOG.md has no versioned heading to compare with Git tags.") sys.exit(1) - current_version, current_status = heading.groups() - current_status = current_status.strip() - if current_status.upper() != "TBD" and not re.fullmatch(r"\d{4}-\d{2}-\d{2}", current_status): - print(f"::error::Unsupported CHANGELOG.md version status: {current_status}") + current_version = heading.group(1) + status_match = re.fullmatch(r" - (TBD|\d{4}-\d{2}-\d{2})[ \t]*", heading.group(2), re.IGNORECASE) + if not status_match: + print(f"::error::Unsupported or missing CHANGELOG.md version status: {heading.group(2).strip()}") sys.exit(1) - - if current_version == next_version and current_status.upper() == "TBD": - # Reuse an already pending next release, such as [5.0.13] - TBD. - new_version = next_version - elif current_version == latest_version and current_status.upper() != "TBD": + current_status = status_match.group(1) + if current_status.upper() != "TBD": + try: + date.fromisoformat(current_status) + except ValueError: + print(f"::error::Invalid CHANGELOG.md release date: {current_status}") + sys.exit(1) + + pending_version = tuple(int(part) for part in current_version.split(".")) + released_version = (major, minor, patch) + if pending_version > released_version: + # Preserve an explicitly planned patch, minor, or major release. + new_version = current_version + elif pending_version == released_version and current_status.upper() != "TBD": # The changelog is at the latest released tag; start the next patch release. new_version = next_version else: print( f"::error::CHANGELOG.md starts at {current_version} ({current_status}), " f"but the latest stable Git tag is {latest_tag}. Expected either " - f"[{latest_version}] with a release date or [{next_version}] - TBD." + f"[{latest_version}] with a release date or a newer version with a date or TBD." ) sys.exit(1) @@ -105,6 +115,7 @@ jobs: output.write(f"current={current_version}\n") output.write(f"new={new_version}\n") output.write(f"branch={branch}\n") + PY - name: Generate codebelt-aicia token id: app-token @@ -140,9 +151,9 @@ jobs: service_note = "This release also includes a service update focused on package dependencies." with open("CHANGELOG.md", encoding="utf-8") as f: content = f.read() - heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - (TBD|\d{4}-\d{2}-\d{2})[ \t]*$", content) + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - (TBD|\d{4}-\d{2}-\d{2})[ \t]*$", content, re.IGNORECASE) if heading and heading.group(1) == new_ver: - if heading.group(2) == "TBD": + if heading.group(2).upper() == "TBD": content = content[:heading.start(2)] + today + content[heading.end(2):] heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - \d{4}-\d{2}-\d{2}[ \t]*$", content) section_end = re.search(r"(?m)^## \[", content[heading.end():]) From ebc91a03081abb300eaf1188124788887b8638ea Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 3 Nov 2026 23:03:04 +0100 Subject: [PATCH 16/20] =?UTF-8?q?=F0=9F=90=B3=20cache=20fingerprinted=20do?= =?UTF-8?q?cfx=20assets=20only=20on=20successful=20responses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Error responses for fingerprinted assets are no longer cached for a year, and range requests are disabled so that a partial response cannot be served from the cache. --- .docfx/nginx.conf | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.docfx/nginx.conf b/.docfx/nginx.conf index c95d1b1..975e107 100644 --- a/.docfx/nginx.conf +++ b/.docfx/nginx.conf @@ -1,3 +1,10 @@ +# Only successful asset responses and revalidation responses may be cached. +map $status $fingerprinted_asset_cache_control { + default "no-store"; + ~^2 "public, max-age=31536000, immutable"; + 304 "public, max-age=31536000, immutable"; +} + server { listen 8080; server_name _; @@ -11,7 +18,9 @@ server { # cached permanently because changed content is published under a different URL. location ~ "^/public/.+-[A-Z0-9]{8}(?:-[A-Z0-9]{8})?(?:\.min)?\.(?:(?:css|js)(?:\.map)?|woff2?)$" { try_files $uri =404; - add_header Cache-Control "public, max-age=31536000, immutable" always; + # Range errors are generated after headers are set, so serve complete assets. + max_ranges 0; + add_header Cache-Control $fingerprinted_asset_cache_control always; add_header Vary "Accept-Encoding" always; } From 96b085fc1132fed7cfd95e6eec2ed552142a3650 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 3 Nov 2026 23:03:04 +0100 Subject: [PATCH 17/20] =?UTF-8?q?=F0=9F=93=A6=20add=20sonaranalyzer=20and?= =?UTF-8?q?=20bump=20central=20package=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SonarAnalyzer.CSharp is added to source projects, and central package versions are bumped for Codebelt.Coverlet.MTP, Cuemon.Core, and Microsoft.Testing.Extensions.HangDump. --- Directory.Build.props | 227 ++++++++++++++++++++------------------- Directory.Packages.props | 9 +- 2 files changed, 119 insertions(+), 117 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index ea668f7..6dc7e72 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,113 +1,114 @@ - - - $(MSBuildProjectName.EndsWith('Tests')) - $(MSBuildProjectName.EndsWith('Benchmarks')) - $(MSBuildProjectDirectory.ToLower().StartsWith('$(MSBuildThisFileDirectory.ToLower())src')) - $(MSBuildProjectDirectory.ToLower().StartsWith('$(MSBuildThisFileDirectory.ToLower())tooling')) - $([MSBuild]::IsOSPlatform('Linux')) - $([MSBuild]::IsOSPlatform('Windows')) - true - false - ..\..\.nuget\$(MSBuildProjectName)\PackageReleaseNotes.txt - latest - - - - true - true - - - - net11.0;net10.0 - Copyright © Geekle 2025-2026. All rights reserved. - gimlichael - Geekle - Extensions for BenchmarkDotNet API by Codebelt - icon.png - README.md - https://benchmarkdotnet.codebelt.net/ - MIT - https://github.com/codebeltnet/benchmarkdotnet - git - en-US - true - true - true - snupkg - true - true - $(MSBuildThisFileDirectory)benchmarkdotnet.snk - true - latest - Recommended - 7035,CA2260,S6618 - v - true - - - - - - - - - - - - - - net11.0;net10.0 - false - Exe - false - false - false - true - none - NU1701,NU1902,NU1903 - false - false - true - - - - 0 - - - - false - Exe - true - - - - - - - - - - - - - - - - - net11.0;net10.0 - false - false - false - false - true - none - NU1701,NU1902,NU1903 - false - false - - - - - - - + + + $(MSBuildProjectName.EndsWith('Tests')) + $(MSBuildProjectName.EndsWith('Benchmarks')) + $(MSBuildProjectDirectory.ToLower().StartsWith('$(MSBuildThisFileDirectory.ToLower())src')) + $(MSBuildProjectDirectory.ToLower().StartsWith('$(MSBuildThisFileDirectory.ToLower())tooling')) + $([MSBuild]::IsOSPlatform('Linux')) + $([MSBuild]::IsOSPlatform('Windows')) + true + false + ..\..\.nuget\$(MSBuildProjectName)\PackageReleaseNotes.txt + latest + + + + true + true + + + + net11.0;net10.0 + Copyright © Geekle 2025-2026. All rights reserved. + gimlichael + Geekle + Extensions for BenchmarkDotNet API by Codebelt + icon.png + README.md + https://benchmarkdotnet.codebelt.net/ + MIT + https://github.com/codebeltnet/benchmarkdotnet + git + en-US + true + true + true + snupkg + true + true + $(MSBuildThisFileDirectory)benchmarkdotnet.snk + true + latest + Recommended + 7035,CA2260,S6618 + v + true + + + + + + + + + + + + + + + net11.0;net10.0 + false + Exe + false + false + false + true + none + NU1701,NU1902,NU1903 + false + false + true + + + + 0 + + + + false + Exe + true + + + + + + + + + + + + + + + + + net11.0;net10.0 + false + false + false + false + true + none + NU1701,NU1902,NU1903 + false + false + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 43fa915..7e97234 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,17 +3,18 @@ true - + - + - + + - \ No newline at end of file + From 1e741a56d52a3332d66cd937c7f601306a52dbc6 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 3 Nov 2026 23:03:04 +0100 Subject: [PATCH 18/20] =?UTF-8?q?=F0=9F=94=A7=20require=20file-scoped=20na?= =?UTF-8?q?mespaces=20in=20editorconfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The editor configuration now treats file-scoped namespaces as an error for all C# files instead of a suggestion, so the rule is enforced rather than optional. --- .editorconfig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index a083db5..886ea15 100644 --- a/.editorconfig +++ b/.editorconfig @@ -37,7 +37,7 @@ dotnet_diagnostic.IDE0063.severity = none dotnet_diagnostic.IDE0071.severity = none # S3267: Loops should be simplified with "LINQ" expressions -# https://rules.sonarsource.com/csharp/RSPEC-3267 +# https://sonarcloud.io/organizations/sonarcloud/rules?open=csharpsquid:S3267&rule_key=csharpsquid:S3267 dotnet_diagnostic.S3267.severity = none # CA1859: Use concrete types when possible for improved performance @@ -57,9 +57,10 @@ indent_size = 4 # IDE0161: Namespace declaration preferences # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0161 -# Prefer file-scoped namespaces for new files; existing block-scoped files should not be converted unless explicitly asked +# File-scoped namespaces are mandatory for new and existing C# files. [*.cs] -csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_namespace_declarations = file_scoped:error +dotnet_diagnostic.IDE0161.severity = error # Top-level statements: DO NOT USE # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0210-ide0211 From 38426c54a2a1f095502b23cf322e6389c2c5206b Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 3 Nov 2026 23:03:05 +0100 Subject: [PATCH 19/20] =?UTF-8?q?=F0=9F=92=AC=20document=20net11=20support?= =?UTF-8?q?=20in=20changelog=20and=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The changelog and package release notes now describe .NET 11 support and the removal of .NET 9, so consumers can see the breaking change for the 2.0.0 release. The contributing guide and the .bot README are updated to match the new test environments and policy. --- .bot/README.md | 20 +- .github/CONTRIBUTING.md | 159 ++++++------ .../PackageReleaseNotes.txt | 231 +++++++++--------- .../PackageReleaseNotes.txt | 227 ++++++++--------- CHANGELOG.md | 19 ++ 5 files changed, 341 insertions(+), 315 deletions(-) diff --git a/.bot/README.md b/.bot/README.md index f3aa97d..9f9afda 100644 --- a/.bot/README.md +++ b/.bot/README.md @@ -1,10 +1,10 @@ -# .bot Workspace - -This folder is reserved for local-only AI working material such as: - -- brainstorm notes -- draft implementation plans -- design alternatives -- temporary agent state - -Keep this folder out of source control. Move only finalized, non-confidential guidance into `AGENTS.md`. +# .bot Workspace + +This folder is reserved for local-only AI working material such as: + +- brainstorm notes +- draft implementation plans +- design alternatives +- temporary agent state + +The `.bot/README.md` file itself is intentionally tracked in source control. All other `.bot/` working material remains local-only and excluded from source control. Move only finalized, non-confidential guidance into `AGENTS.md`. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a14d35f..70e024e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,79 +1,80 @@ -# Contributing to Extensions for BenchmarkDotNet API by Codebelt - -This repository is part of the Codebelt .NET library estate. The instructions below describe the current checkout and its CI contract. Please keep changes focused and preserve the shared Codebelt build skeleton unless a deliberate policy change is being made. - -## Before you start - -- Read the repository `README.md` and open an issue before starting a non-trivial feature or behavioral change. -- Use an installed .NET SDK that can build the target frameworks listed below. This repository currently targets: **net11.0, net10.0**. -- The solution is `Codebelt.Extensions.BenchmarkDotNet.slnx`. Central package versions are maintained in `Directory.Packages.props`. -- The shared build behavior is in `Directory.Build.props` and `Directory.Build.targets`; repository-specific TFMs, package references and metadata remain local to this library. - -## Repository shape - -- `src/` contains production projects. -- `test/` contains xUnit v3 test projects. -- `Codebelt.Extensions.BenchmarkDotNet.slnx` is the solution used for local development. -- `.github/workflows/ci-pipeline.yml` is the CI workflow and the authority for the test matrix. -- `testenvironments.json` declares the supported `WSL-Ubuntu` and `Docker-Ubuntu` test environments. - -## Build - -Restore and build the solution from the repository root: - -```powershell -dotnet restore "Codebelt.Extensions.BenchmarkDotNet.slnx" -dotnet build "Codebelt.Extensions.BenchmarkDotNet.slnx" --configuration Release --no-restore -``` - -CI builds both Debug and Release configurations. A clean build should complete before opening a pull request. - -## Test - -Run tests one project at a time so a failing or hanging project is attributable. This mirrors the CI matrix; it does not silently turn skipped integration tests into passing tests. - -```powershell -$testProjects = Get-ChildItem test -Filter *.csproj -Recurse -foreach ($project in $testProjects) { - dotnet test $project.FullName --configuration Release --no-restore -} -``` - -The CI test plan currently runs **4** project(s) and excludes **0** project(s). The workflow also has an optional macOS test job. - -## Integration and infrastructure - -- `WSL-Ubuntu` — WSL distribution `Ubuntu-24.04`. -- `Docker-Ubuntu` — Docker image `codebeltnet/ubuntu-testrunner:8-9-10-11`. - -This repository has no repository-local `docker-compose.yml` service dependency in the current checkout. -Use the environments declared in `testenvironments.json` when you need the estate test runner. - -## Package and documentation - -Create packages using the same solution and Release configuration: - -```powershell -dotnet pack "Codebelt.Extensions.BenchmarkDotNet.slnx" --configuration Release --no-restore -``` - -Package-specific release notes live under `.nuget//PackageReleaseNotes.txt` and package README files live beside them. `Directory.Build.targets` imports the release notes during packing. Public API changes also require XML documentation updates; DocFX documentation is built by the repository automation. - -## Pull requests - -1. Create or join an issue before substantial work, then fork the repository and create a branch from `main`. -2. Add or update focused tests and public API documentation where applicable. -3. Run restore, build, and the relevant per-project tests locally. -4. Keep the pull request small, explain the behavior change and validation performed, and wait for the CI checks to pass. - -## Issues - -Include the affected project, target framework, operating system, SDK version, exact command, expected result, actual result, and a minimal reproduction. Identify whether the behavior differs between local Windows/WSL, Docker-Ubuntu and GitHub Actions. - -## Coding guidelines - -Follow the existing style, the Framework Design Guidelines, the repository `.editorconfig`, and the shared Codebelt conventions. Do not make unrelated formatting or infrastructure changes in a feature pull request. - -## License - -By contributing to Extensions for BenchmarkDotNet API by Codebelt, you agree that your contributions will be licensed under the MIT license. +# Contributing to Extensions for BenchmarkDotNet API by Codebelt + +This repository is part of the Codebelt .NET library estate. The instructions below describe the current checkout and its CI contract. Please keep changes focused and preserve the shared Codebelt build skeleton unless a deliberate policy change is being made. + +## Before you start + +- Read the repository `README.md` and open an issue before starting a non-trivial feature or behavioral change. +- Use an installed .NET SDK that can build the target frameworks listed below. This repository currently targets: **net11.0, net10.0**. +- The solution is `Codebelt.Extensions.BenchmarkDotNet.slnx`. Central package versions are maintained in `Directory.Packages.props`. +- The shared build behavior is in `Directory.Build.props` and `Directory.Build.targets`; repository-specific TFMs, package references and metadata remain local to this library. + +## Repository shape + +- `src/` contains production projects. +- `test/` contains xUnit v3 test projects. +- `Codebelt.Extensions.BenchmarkDotNet.slnx` is the solution used for local development. +- `.github/workflows/ci-pipeline.yml` is the CI workflow and the authority for the test matrix. +- `testenvironments.json` declares the supported `WSL-Ubuntu`, `Docker-Ubuntu (net10.0)`, and `Docker-Ubuntu (net11.0)` test environments. + +## Build + +Restore and build the solution from the repository root: + +```powershell +dotnet restore "Codebelt.Extensions.BenchmarkDotNet.slnx" +dotnet build "Codebelt.Extensions.BenchmarkDotNet.slnx" --configuration Release --no-restore +``` + +CI builds both Debug and Release configurations. A clean build should complete before opening a pull request. + +## Test + +Run tests one project at a time so a failing or hanging project is attributable. This mirrors the CI matrix; it does not silently turn skipped integration tests into passing tests. + +```powershell +$testProjects = Get-ChildItem test -Filter *.csproj -Recurse +foreach ($project in $testProjects) { + dotnet test --project $project.FullName --configuration Release --no-restore +} +``` + +The CI test plan currently runs **4** project(s) and excludes **0** project(s). The workflow also has an optional macOS test job. + +## Integration and infrastructure + +- `WSL-Ubuntu` — WSL distribution `Ubuntu-24.04`. +- `Docker-Ubuntu (net10.0)` — Docker image `codebeltnet/ubuntu-testrunner:10`. +- `Docker-Ubuntu (net11.0)` — Docker image `codebeltnet/ubuntu-testrunner:11`. + +This repository has no repository-local `docker-compose.yml` service dependency in the current checkout. +Use the environments declared in `testenvironments.json` when you need the estate test runner. + +## Package and documentation + +Create packages using the same solution and Release configuration: + +```powershell +dotnet pack "Codebelt.Extensions.BenchmarkDotNet.slnx" --configuration Release --no-restore +``` + +Package-specific release notes live under `.nuget//PackageReleaseNotes.txt` and package README files live beside them. `Directory.Build.targets` imports the release notes during packing. Public API changes also require XML documentation updates; DocFX documentation is built by the repository automation. + +## Pull requests + +1. Create or join an issue before substantial work, then fork the repository and create a branch from `main`. +2. Add or update focused tests and public API documentation where applicable. +3. Run restore, build, and the relevant per-project tests locally. +4. Keep the pull request small, explain the behavior change and validation performed, and wait for the CI checks to pass. + +## Issues + +Include the affected project, target framework, operating system, SDK version, exact command, expected result, actual result, and a minimal reproduction. Identify whether the behavior differs between local Windows/WSL, Docker-Ubuntu and GitHub Actions. + +## Coding guidelines + +Follow the existing style, the Framework Design Guidelines, the repository `.editorconfig`, and the shared Codebelt conventions. Do not make unrelated formatting or infrastructure changes in a feature pull request. + +## License + +By contributing to Extensions for BenchmarkDotNet API by Codebelt, you agree that your contributions will be licensed under the MIT license. diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt index 02121e0..2a9f718 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt @@ -1,114 +1,117 @@ -Version: 2.0.0 -Availability: .NET 11 and .NET 10 - -# ALM -- CHANGED Target frameworks by adding .NET 11 (net11.0) and removing .NET 9 (net9.0), so the package now targets .NET 11 and .NET 10 - -Version: 1.3.5 -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.3.4 -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.3.3 -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.3.2 -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.3.1 -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.3.0 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Microsoft.NET.Test.Sdk has been upgraded from version 18.5.1 to 18.6.0 for improved test framework reliability - -# Improvements -- EXTENDED BenchmarkProgram class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to include RunAsync equivalent methods of the existing Run methods for improved asynchronous hosting scenarios - -Version: 1.2.7 -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.2.6 -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.2.5 -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.2.4 -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.2.3 -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.2.2 -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.2.1 -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.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 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -# Improvements -- EXTENDED BenchmarkProgram class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to support an optional service configurator delegate for customizing the IServiceCollection during host building -- EXTENDED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to support skipping benchmarks that already have generated reports based on the BenchmarkWorkspaceOptions.SkipBenchmarksWithReports property -- CHANGED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to conditionally suppress console status messages in services based on whether you are in a debugging session or not - -Version: 1.0.0 -Availability: .NET 10 and .NET 9 - -# New Features -- ADDED BenchmarkContext class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that represents the command-line context for a benchmark run -- ADDED BenchmarkProgram class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that provides the main entry point for hosting and running benchmarks using BenchmarkDotNet -- ADDED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that is responsible for executing benchmarks within the console host +Version: 2.0.0 +Availability: .NET 11 and .NET 10 + +# ALM +- CHANGED Target frameworks by adding .NET 11 (net11.0) and removing .NET 9 (net9.0), so the package now targets .NET 11 and .NET 10 + +# Breaking Changes +- .NET 9 consumers must target .NET 10 or later. + +Version: 1.3.5 +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.3.4 +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.3.3 +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.3.2 +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.3.1 +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.3.0 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Microsoft.NET.Test.Sdk has been upgraded from version 18.5.1 to 18.6.0 for improved test framework reliability + +# Improvements +- EXTENDED BenchmarkProgram class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to include RunAsync equivalent methods of the existing Run methods for improved asynchronous hosting scenarios + +Version: 1.2.7 +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.2.6 +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.2.5 +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.2.4 +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.2.3 +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.2.2 +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.2.1 +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.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 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +# Improvements +- EXTENDED BenchmarkProgram class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to support an optional service configurator delegate for customizing the IServiceCollection during host building +- EXTENDED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to support skipping benchmarks that already have generated reports based on the BenchmarkWorkspaceOptions.SkipBenchmarksWithReports property +- CHANGED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to conditionally suppress console status messages in services based on whether you are in a debugging session or not + +Version: 1.0.0 +Availability: .NET 10 and .NET 9 + +# New Features +- ADDED BenchmarkContext class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that represents the command-line context for a benchmark run +- ADDED BenchmarkProgram class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that provides the main entry point for hosting and running benchmarks using BenchmarkDotNet +- ADDED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that is responsible for executing benchmarks within the console host diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt index 01db84b..b1d93af 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt @@ -1,112 +1,115 @@ -Version: 2.0.0 -Availability: .NET 11 and .NET 10 - -# ALM -- CHANGED Target frameworks by adding .NET 11 (net11.0) and removing .NET 9 (net9.0), so the package now targets .NET 11 and .NET 10 - -Version: 1.3.5 -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.3.4 -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.3.3 -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.3.2 -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.3.1 -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.3.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.2.7 -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.2.6 -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.2.5 -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.2.4 -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.2.3 -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.2.2 -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.2.1 -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.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 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -# Bug Fixes -- FIXED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace so it no longer relies on Danish culture - -# Improvements -- EXTENDED BenchmarkWorkspace class in the Codebelt.Extensions.BenchmarkDotNet namespace with two new static methods; GetReportsResultsPath and GetReportsTuningPath for retrieving the paths to the reports results and tuning directories respectively -- EXTENDED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace to include one new property; SkipBenchmarksWithReports that indicates whether benchmarks that already have generated reports should be skipped during execution - -Version: 1.0.0 -Availability: .NET 10 and .NET 9 - -# New Features -- ADDED BenchmarkWorkspace class in the Codebelt.Extensions.BenchmarkDotNet namespace that provides a default implementation of IBenchmarkWorkspace for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet -- ADDED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace that specifies configuration options that is related to the BenchmarkWorkspace class -- ADDED BenchmarkWorkspaceOptionsExtensions class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the BenchmarkWorkspaceOptions class: ConfigureBenchmarkDotNet -- ADDED IBenchmarkWorkspace interface in the Codebelt.Extensions.BenchmarkDotNet namespace that defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet -- ADDED ServiceCollectionExtensions class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the IServiceCollection interface: AddBenchmarkWorkspace and AddBenchmarkWorkspace{TWorkspace} +Version: 2.0.0 +Availability: .NET 11 and .NET 10 + +# ALM +- CHANGED Target frameworks by adding .NET 11 (net11.0) and removing .NET 9 (net9.0), so the package now targets .NET 11 and .NET 10 + +# Breaking Changes +- .NET 9 consumers must target .NET 10 or later. + +Version: 1.3.5 +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.3.4 +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.3.3 +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.3.2 +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.3.1 +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.3.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.2.7 +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.2.6 +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.2.5 +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.2.4 +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.2.3 +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.2.2 +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.2.1 +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.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 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +# Bug Fixes +- FIXED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace so it no longer relies on Danish culture + +# Improvements +- EXTENDED BenchmarkWorkspace class in the Codebelt.Extensions.BenchmarkDotNet namespace with two new static methods; GetReportsResultsPath and GetReportsTuningPath for retrieving the paths to the reports results and tuning directories respectively +- EXTENDED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace to include one new property; SkipBenchmarksWithReports that indicates whether benchmarks that already have generated reports should be skipped during execution + +Version: 1.0.0 +Availability: .NET 10 and .NET 9 + +# New Features +- ADDED BenchmarkWorkspace class in the Codebelt.Extensions.BenchmarkDotNet namespace that provides a default implementation of IBenchmarkWorkspace for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet +- ADDED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace that specifies configuration options that is related to the BenchmarkWorkspace class +- ADDED BenchmarkWorkspaceOptionsExtensions class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the BenchmarkWorkspaceOptions class: ConfigureBenchmarkDotNet +- ADDED IBenchmarkWorkspace interface in the Codebelt.Extensions.BenchmarkDotNet namespace that defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet +- ADDED ServiceCollectionExtensions class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the IServiceCollection interface: AddBenchmarkWorkspace and AddBenchmarkWorkspace{TWorkspace} diff --git a/CHANGELOG.md b/CHANGELOG.md index 183e6c6..3574cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ 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. +## [2.0.0] - 2026-11-10 + +This is a major release because it removes .NET 9 (`net9.0`) as a supported target framework, so the packages now target `.NET 11` and `.NET 10`. It also updates the documentation and examples to the new target frameworks and refreshes two central build dependencies. + +### Added + +- Added `net11.0` as a target framework for the `Codebelt.Extensions.BenchmarkDotNet` and `Codebelt.Extensions.BenchmarkDotNet.Console` packages. + +### Changed + +- Updated the repository and package READMEs to describe `.NET 11` and `.NET 10` support, and changed the console example to benchmark `.NET 10` and `.NET 11` jobs instead of `.NET 9`, +- Updated `Codebelt.Coverlet.MTP` from 10.1.0 to 10.1.1 and `Codebelt.Bootstrapper.Console` from 5.2.2 to 5.2.3 in the central package versions, +- Reformatted the C# source files to file-scoped namespaces and removed UTF-8 byte order marks and trailing whitespace, with no behavior or API changes. + +### Removed + +- Removed `net9.0` as a target framework for the `Codebelt.Extensions.BenchmarkDotNet` and `Codebelt.Extensions.BenchmarkDotNet.Console` packages, so consumers on .NET 9 are no longer supported. + ## [1.3.5] - 2026-10-17 This is a patch release that updates dependencies, consolidates agent guidance and revises documentation hosting. @@ -195,6 +213,7 @@ This is the initial stable release of the `Codebelt.Extensions.BenchmarkDotNet` - ADDED `BenchmarkProgram` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that provides the main entry point for hosting and running benchmarks using BenchmarkDotNet, - ADDED `BenchmarkWorker` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that is responsible for executing benchmarks within the console host. +[2.0.0]: https://github.com/codebeltnet/benchmarkdotnet/compare/v1.3.5...v2.0.0 [1.3.5]: https://github.com/codebeltnet/benchmarkdotnet/compare/v1.3.4...v1.3.5 [1.3.4]: https://github.com/codebeltnet/benchmarkdotnet/compare/v1.3.3...v1.3.4 [1.3.3]: https://github.com/codebeltnet/benchmarkdotnet/compare/v1.3.2...v1.3.3 From a0904ff7cc70cd19594ad1de649b5ed6ce19720f Mon Sep 17 00:00:00 2001 From: gimlichael Date: Wed, 4 Nov 2026 22:19:38 +0100 Subject: [PATCH 20/20] =?UTF-8?q?=F0=9F=94=A7=20update=20scorecard=20workf?= =?UTF-8?q?low=20for=20improved=20analysis=20and=20scheduling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/scorecard.yml | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index aabea97..c00ab61 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,10 +1,12 @@ -name: Scorecard supply-chain security +name: Scorecard analysis workflow on: - branch_protection_rule: - schedule: - - cron: '45 17 * * 2' push: - branches: [ "main" ] + # Only the default branch is supported. + branches: + - main + schedule: + # Weekly on Saturdays. + - cron: '30 1 * * 6' permissions: read-all @@ -13,30 +15,42 @@ jobs: name: Scorecard analysis runs-on: ubuntu-latest permissions: + # Needed for Code scanning upload security-events: write + # Needed for GitHub OIDC token if publish_results is true id-token: write steps: - name: "Checkout code" - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@v2.4.0 + uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0 with: results_file: results.sarif results_format: sarif + # Scorecard team runs a weekly scan of public GitHub repos, + # see https://github.com/ossf/scorecard#public-data. + # Setting `publish_results: true` helps us scale by leveraging your workflow to + # extract the results instead of relying on our own infrastructure to run scans. + # And it's free for you! publish_results: true + # Upload the results as artifacts (optional). Commenting out will disable + # uploads of run results in SARIF format to the repository Actions tab. + # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts - name: "Upload artifact" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: SARIF file path: results.sarif retention-days: 5 + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 with: sarif_file: results.sarif