From 392b0826df930f1ad5a7bac713c6e012f0683ab1 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Thu, 22 Oct 2026 18:01:31 +0200 Subject: [PATCH 01/10] =?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/10] =?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/10] =?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/10] =?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/10] =?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/10] =?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/10] =?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/10] =?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/10] =?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/10] =?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