From 948e28b29a5ec176012ae67aed10ee54cc0b999e Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Mon, 8 Dec 2025 23:51:34 +0100 Subject: [PATCH 01/32] :fire: remove repo template files --- ClassLibrary1.sln | 46 ------------------- src/ClassLibrary1/Class1.cs | 7 --- src/ClassLibrary1/ClassLibrary1.csproj | 5 -- .../Class1Test.cs | 9 ---- .../TestProject1.FunctionalTests.csproj | 11 ----- test/TestProject1/Class1Test.cs | 14 ------ test/TestProject1/TestProject1.Tests.csproj | 11 ----- 7 files changed, 103 deletions(-) delete mode 100644 ClassLibrary1.sln delete mode 100644 src/ClassLibrary1/Class1.cs delete mode 100644 src/ClassLibrary1/ClassLibrary1.csproj delete mode 100644 test/TestProject1.FunctionalTests/Class1Test.cs delete mode 100644 test/TestProject1.FunctionalTests/TestProject1.FunctionalTests.csproj delete mode 100644 test/TestProject1/Class1Test.cs delete mode 100644 test/TestProject1/TestProject1.Tests.csproj diff --git a/ClassLibrary1.sln b/ClassLibrary1.sln deleted file mode 100644 index 4f2a9f7..0000000 --- a/ClassLibrary1.sln +++ /dev/null @@ -1,46 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34728.123 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClassLibrary1", "src\ClassLibrary1\ClassLibrary1.csproj", "{A9DFF36B-1AD4-40EC-9394-C720C3DC785A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0070E83B-2DDD-4537-A83F-1CF8644F2880}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A3C56B2E-55EE-44EC-876E-B03B8DDA3317}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject1.Tests", "test\TestProject1\TestProject1.Tests.csproj", "{A7389E99-2E98-4925-8055-3267BBC6C084}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1.FunctionalTests", "test\TestProject1.FunctionalTests\TestProject1.FunctionalTests.csproj", "{507C6397-4FE2-40E8-A8AA-68ED202B48C8}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Release|Any CPU.Build.0 = Release|Any CPU - {A7389E99-2E98-4925-8055-3267BBC6C084}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A7389E99-2E98-4925-8055-3267BBC6C084}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A7389E99-2E98-4925-8055-3267BBC6C084}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A7389E99-2E98-4925-8055-3267BBC6C084}.Release|Any CPU.Build.0 = Release|Any CPU - {507C6397-4FE2-40E8-A8AA-68ED202B48C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {507C6397-4FE2-40E8-A8AA-68ED202B48C8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {507C6397-4FE2-40E8-A8AA-68ED202B48C8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {507C6397-4FE2-40E8-A8AA-68ED202B48C8}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A} = {0070E83B-2DDD-4537-A83F-1CF8644F2880} - {A7389E99-2E98-4925-8055-3267BBC6C084} = {A3C56B2E-55EE-44EC-876E-B03B8DDA3317} - {507C6397-4FE2-40E8-A8AA-68ED202B48C8} = {A3C56B2E-55EE-44EC-876E-B03B8DDA3317} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {0CBE2805-F0FF-4D0F-902C-8B9277A5D3F2} - EndGlobalSection -EndGlobal diff --git a/src/ClassLibrary1/Class1.cs b/src/ClassLibrary1/Class1.cs deleted file mode 100644 index 68fb1be..0000000 --- a/src/ClassLibrary1/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ClassLibrary1 -{ - public class Class1 - { - - } -} diff --git a/src/ClassLibrary1/ClassLibrary1.csproj b/src/ClassLibrary1/ClassLibrary1.csproj deleted file mode 100644 index 88c8dc6..0000000 --- a/src/ClassLibrary1/ClassLibrary1.csproj +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/test/TestProject1.FunctionalTests/Class1Test.cs b/test/TestProject1.FunctionalTests/Class1Test.cs deleted file mode 100644 index 2ec9e90..0000000 --- a/test/TestProject1.FunctionalTests/Class1Test.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Codebelt.Extensions.Xunit; - -namespace ClassLibrary1 -{ - public class Class1Test : Test - { - - } -} diff --git a/test/TestProject1.FunctionalTests/TestProject1.FunctionalTests.csproj b/test/TestProject1.FunctionalTests/TestProject1.FunctionalTests.csproj deleted file mode 100644 index 7bb358a..0000000 --- a/test/TestProject1.FunctionalTests/TestProject1.FunctionalTests.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - Classlibrary1 - - - - - - - diff --git a/test/TestProject1/Class1Test.cs b/test/TestProject1/Class1Test.cs deleted file mode 100644 index 9790743..0000000 --- a/test/TestProject1/Class1Test.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Codebelt.Extensions.Xunit; -using Xunit; - -namespace Classlibrary1 -{ - public class Class1Test : Test - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/test/TestProject1/TestProject1.Tests.csproj b/test/TestProject1/TestProject1.Tests.csproj deleted file mode 100644 index 7bb358a..0000000 --- a/test/TestProject1/TestProject1.Tests.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - Classlibrary1 - - - - - - - From 856dbca064fa524769e4369c671c07c173921e9a Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Mon, 8 Dec 2025 23:53:47 +0100 Subject: [PATCH 02/32] :sparkles: add extensibility layer for benchmarkdotnet --- .../BenchmarkWorkspace.cs | 173 +++++ .../BenchmarkWorkspaceOptions.cs | 310 +++++++++ .../BenchmarkWorkspaceOptionsExtensions.cs | 71 ++ ...Codebelt.Extensions.BenchmarkDotNet.csproj | 8 + .../IBenchmarkWorkspace.cs | 20 + .../ServiceCollectionExtensions.cs | 45 ++ .../BenchmarkWorkspaceOptionsTest.cs | 487 ++++++++++++++ .../BenchmarkWorkspaceTest.cs | 630 ++++++++++++++++++ ...lt.Extensions.BenchmarkDotNet.Tests.csproj | 17 + .../ServiceCollectionExtensionsTest.cs | 56 ++ 10 files changed, 1817 insertions(+) create mode 100644 src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs create mode 100644 src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs create mode 100644 src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs create mode 100644 src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj create mode 100644 src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs create mode 100644 src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs create mode 100644 test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs create mode 100644 test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs create mode 100644 test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj create mode 100644 test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs new file mode 100644 index 0000000..74fd85c --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs @@ -0,0 +1,173 @@ +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; + +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; + + /// + /// 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() + { + var reportsResultsPath = Path.Combine(_options.Configuration.ArtifactsPath, "results"); + var reportsTuningPath = Path.Combine(_options.Configuration.ArtifactsPath, _options.RepositoryTuningFolder); + CleanupResults(reportsResultsPath, reportsTuningPath); + } + + private static IEnumerable LoadAssemblies(string repositoryPath, string targetFrameworkMoniker, string benchmarkProjectSuffix, string repositoryTuningFolder, bool useDebugBuild) + { + var tuningDir = Path.Combine(repositoryPath, repositoryTuningFolder); + + if (!Directory.Exists(tuningDir)) + { + Directory.CreateDirectory(tuningDir); + } + + if (!_assemblyResolverRegistered) + { + AppDomain.CurrentDomain.AssemblyResolve += (_, args) => + { + try + { + var requestedName = new AssemblyName(args.Name).Name; + if (string.IsNullOrEmpty(requestedName)) { return null; } + var fileName = requestedName + ".dll"; + + var match = Directory.EnumerateFiles(tuningDir, fileName, SearchOption.AllDirectories).FirstOrDefault(); + if (match != null) + { + var asmName = AssemblyName.GetAssemblyName(match); + var already = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), asmName)); + if (already != null) { return already; } + return Assembly.LoadFrom(match); + } + } + catch + { + // swallow and allow default resolution to continue + } + return null; + }; + _assemblyResolverRegistered = true; + } + + var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies(); + + var assemblies = new List(); + var filteredAssemblies = Directory.EnumerateFiles(tuningDir, $"*.{benchmarkProjectSuffix}.dll", SearchOption.AllDirectories); + var debugOrRelease = useDebugBuild + ? "Debug" + : "Release"; + + foreach (var path in filteredAssemblies.Where(path => path.Contains($"bin{Path.DirectorySeparatorChar}{debugOrRelease}{Path.DirectorySeparatorChar}{targetFrameworkMoniker}", StringComparison.OrdinalIgnoreCase))) + { + 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 + { + // intentionally swallow to allow other assemblies to load + } + } + + 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 CleanupResults(string reportsResultsPath, string reportsTuningPath) + { + if (!Directory.Exists(reportsResultsPath)) { return; } + + Directory.CreateDirectory(reportsTuningPath); + + foreach (var file in Directory.GetFiles(reportsResultsPath).Where(s => !s.EndsWith(".lock"))) + { + var targetFile = Path.Combine(reportsTuningPath, Path.GetFileName(file)); + File.Delete(targetFile); + File.Move(file, targetFile); + } + + Directory.Delete(reportsResultsPath, recursive: true); + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs new file mode 100644 index 0000000..3321d0a --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs @@ -0,0 +1,310 @@ +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.Globalization; +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 +/// +/// +/// +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(); + private static readonly CultureInfo DanishCulture = CultureInfo.GetCultureInfo("da-DK"); + + /// + /// 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; } + + /// + /// 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).WithCultureInfo(DanishCulture)); // 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 ResolveCurrentTfm() + { + try + { + var entry = Assembly.GetEntryAssembly(); + var tfa = entry?.GetCustomAttribute(); + if (!string.IsNullOrEmpty(tfa?.FrameworkName)) + { + var fn = new FrameworkName(tfa.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 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 new file mode 100644 index 0000000..0f9c12a --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs @@ -0,0 +1,71 @@ +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. + /// + /// + 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/Codebelt.Extensions.BenchmarkDotNet.csproj b/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj new file mode 100644 index 0000000..9e03c77 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs new file mode 100644 index 0000000..b340c46 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs @@ -0,0 +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(); +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..bcaf7c1 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs @@ -0,0 +1,45 @@ +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 TImplementation : 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/BenchmarkWorkspaceOptionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs new file mode 100644 index 0000000..929eda3 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs @@ -0,0 +1,487 @@ +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.Xunit; +using System; +using System.IO; +using System.Linq; +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); + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs new file mode 100644 index 0000000..9f07e74 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs @@ -0,0 +1,630 @@ +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); + } + } + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj new file mode 100644 index 0000000..28bfdd5 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj @@ -0,0 +1,17 @@ + + + + Codebelt.Extensions.BenchmarkDotNet + + + + + false + + + + + + + + diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs new file mode 100644 index 0000000..86907e9 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs @@ -0,0 +1,56 @@ +using System; +using System.Reflection; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +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); + } + + private sealed class FakeWorkspace : IBenchmarkWorkspace + { + public Assembly[] LoadBenchmarkAssemblies() => Array.Empty(); + + public void PostProcessArtifacts() { } + } + } +} From 69ad4fefd75cc0c58f8a80bee43d2b53f676521e Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 9 Dec 2025 00:22:13 +0100 Subject: [PATCH 03/32] :test_tube: add comprehensive tests for BenchmarkWorkspaceOptions --- .../BenchmarkWorkspaceOptionsTest.cs | 1128 ++++++++++------- 1 file changed, 645 insertions(+), 483 deletions(-) diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs index 929eda3..2199328 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs @@ -1,487 +1,649 @@ -using BenchmarkDotNet.Configs; -using Codebelt.Extensions.Xunit; -using System; -using System.IO; -using System.Linq; -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); +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.Xunit; +using System; +using System.IO; +using System.Linq; +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); + 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); - } -} + 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_ShouldHaveDanishCultureInfo() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.Configuration.SummaryStyle); + Assert.NotNull(options.Configuration.SummaryStyle.CultureInfo); + Assert.Equal("da-DK", options.Configuration.SummaryStyle.CultureInfo.Name); + + TestOutput.WriteLine($"CultureInfo: {options.Configuration.SummaryStyle.CultureInfo.Name}"); + } + + [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}"); + } +} From e7ec032a3a8e3781b0d78dc848a2049328a80af8 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 9 Dec 2025 01:07:14 +0100 Subject: [PATCH 04/32] :rocket: add dedicated benchmarking suite and runner projects --- ...t.Extensions.BenchmarkDotNet.Runner.csproj | 25 ++++ .../Program.cs | 24 ++++ .../BenchmarkWorkspaceBenchmark.cs | 72 +++++++++++ .../BenchmarkWorkspaceOptionsBenchmark.cs | 114 ++++++++++++++++++ ...tensions.BenchmarkDotNet.Benchmarks.csproj | 11 ++ 5 files changed, 246 insertions(+) create mode 100644 tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj create mode 100644 tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs create mode 100644 tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs create mode 100644 tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs create mode 100644 tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj diff --git a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj new file mode 100644 index 0000000..4f5d4af --- /dev/null +++ b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj @@ -0,0 +1,25 @@ + + + + Exe + net10.0 + dotnet-bdn-runner + $(MSBuildProjectName) + + + + + + + + + false + + + + + + + + + diff --git a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs new file mode 100644 index 0000000..ef47eb4 --- /dev/null +++ b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs @@ -0,0 +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.ConfigureBenchmarkDotNet(c => + { + var slimJob = BenchmarkWorkspaceOptions.Slim; + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) + .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + }); + }); + } + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs new file mode 100644 index 0000000..0371c76 --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs @@ -0,0 +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); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs new file mode 100644 index 0000000..0006210 --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs @@ -0,0 +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); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj new file mode 100644 index 0000000..24cde66 --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj @@ -0,0 +1,11 @@ + + + + Codebelt.Extensions.BenchmarkDotNet + + + + + + + From 6aeb59e9de37048d6f8f23a7cf4822a8456ade13 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 9 Dec 2025 01:07:50 +0100 Subject: [PATCH 05/32] :chart_with_upwards_trend: add benchmarkdotnet github reports for workspace --- ...nchmarkWorkspaceBenchmark-report-github.md | 22 ++++++++++ ...WorkspaceOptionsBenchmark-report-github.md | 44 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md create mode 100644 reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md new file mode 100644 index 0000000..2078082 --- /dev/null +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md @@ -0,0 +1,22 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7309) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|-------------------------------------------------------------- |---------- |-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-------:|--------:|-------:|----------:|------------:| +| 'Construct BenchmarkDotNetWorkspace' | .NET 10.0 | 2.163 μs | 0.0318 μs | 0.0282 μs | 2.166 μs | 2.125 μs | 2.206 μs | 1.00 | 0.02 | 0.2659 | 4248 B | 1.00 | +| 'Load assemblies from tuning folder (no matching assemblies)' | .NET 10.0 | 406.714 μs | 17.1587 μs | 19.7600 μs | 409.632 μs | 386.064 μs | 455.586 μs | 188.03 | 9.23 | - | 15913 B | 3.75 | +| 'PostProcessArtifacts (move results -> tuning folder)' | .NET 10.0 | 10.829 μs | 0.2468 μs | 0.2743 μs | 10.775 μs | 10.421 μs | 11.538 μs | 5.01 | 0.14 | - | 392 B | 0.09 | +| | | | | | | | | | | | | | +| 'Construct BenchmarkDotNetWorkspace' | .NET 9.0 | 3.041 μs | 0.0650 μs | 0.0749 μs | 3.053 μs | 2.937 μs | 3.181 μs | 1.00 | 0.03 | 0.2656 | 4272 B | 1.00 | +| 'Load assemblies from tuning folder (no matching assemblies)' | .NET 9.0 | 400.892 μs | 15.9646 μs | 18.3849 μs | 398.322 μs | 380.181 μs | 441.577 μs | 131.89 | 6.70 | - | 15817 B | 3.70 | +| 'PostProcessArtifacts (move results -> tuning folder)' | .NET 9.0 | 10.772 μs | 0.2729 μs | 0.3143 μs | 10.744 μs | 10.320 μs | 11.353 μs | 3.54 | 0.13 | - | 392 B | 0.09 | diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md new file mode 100644 index 0000000..fa89bd8 --- /dev/null +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md @@ -0,0 +1,44 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7309) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------------------------------------- |---------- |--------------:|-----------:|-----------:|--------------:|--------------:|--------------:|------:|--------:|-------:|----------:|------------:| +| 'PostConfigureOptions - default config' | .NET 10.0 | 2,110.8640 ns | 56.8229 ns | 65.4374 ns | 2,095.9841 ns | 2,013.1433 ns | 2,238.9894 ns | ? | ? | 0.2705 | 4248 B | ? | +| 'PostConfigureOptions - custom config' | .NET 10.0 | 2,095.8151 ns | 68.6064 ns | 76.2558 ns | 2,101.2500 ns | 1,977.0449 ns | 2,221.1213 ns | ? | ? | 0.3073 | 4840 B | ? | +| | | | | | | | | | | | | | +| 'PostConfigureOptions - default config' | .NET 9.0 | 2,986.9093 ns | 55.9419 ns | 52.3281 ns | 3,005.3757 ns | 2,905.3320 ns | 3,077.9813 ns | ? | ? | 0.2603 | 4248 B | ? | +| 'PostConfigureOptions - custom config' | .NET 9.0 | 3,044.9700 ns | 24.8852 ns | 20.7803 ns | 3,042.8357 ns | 2,998.1666 ns | 3,071.4844 ns | ? | ? | 0.3000 | 4840 B | ? | +| | | | | | | | | | | | | | +| 'Create default BenchmarkWorkspaceOptions' | .NET 10.0 | 2,072.5817 ns | 46.8350 ns | 52.0570 ns | 2,064.2872 ns | 2,003.2817 ns | 2,172.9578 ns | 1.00 | 0.03 | 0.2550 | 4120 B | 1.00 | +| 'Create and configure BenchmarkWorkspaceOptions' | .NET 10.0 | 2,030.7542 ns | 68.5204 ns | 76.1602 ns | 2,004.4376 ns | 1,941.1354 ns | 2,174.2652 ns | 0.98 | 0.04 | 0.2548 | 4120 B | 1.00 | +| | | | | | | | | | | | | | +| 'Create default BenchmarkWorkspaceOptions' | .NET 9.0 | 2,979.9549 ns | 59.5392 ns | 63.7062 ns | 2,978.6564 ns | 2,892.7934 ns | 3,115.2483 ns | 1.00 | 0.03 | 0.2570 | 4120 B | 1.00 | +| 'Create and configure BenchmarkWorkspaceOptions' | .NET 9.0 | 2,985.9997 ns | 74.5567 ns | 82.8695 ns | 3,015.2933 ns | 2,867.5636 ns | 3,138.8891 ns | 1.00 | 0.03 | 0.2534 | 4120 B | 1.00 | +| | | | | | | | | | | | | | +| 'Full lifecycle - create, configure, validate' | .NET 10.0 | 2,174.6726 ns | 54.0249 ns | 57.8061 ns | 2,180.9386 ns | 2,077.8922 ns | 2,284.7667 ns | ? | ? | 0.2779 | 4464 B | ? | +| | | | | | | | | | | | | | +| 'Full lifecycle - create, configure, validate' | .NET 9.0 | 3,043.1674 ns | 63.6125 ns | 70.7051 ns | 3,018.4952 ns | 2,960.1038 ns | 3,190.1593 ns | ? | ? | 0.2764 | 4464 B | ? | +| | | | | | | | | | | | | | +| 'Property access - RepositoryPath' | .NET 10.0 | 0.6960 ns | 0.0311 ns | 0.0276 ns | 0.6961 ns | 0.6520 ns | 0.7445 ns | ? | ? | - | - | ? | +| 'Property access - Configuration' | .NET 10.0 | 0.6842 ns | 0.0374 ns | 0.0312 ns | 0.6787 ns | 0.6287 ns | 0.7299 ns | ? | ? | - | - | ? | +| | | | | | | | | | | | | | +| 'Property access - RepositoryPath' | .NET 9.0 | 0.7740 ns | 0.0509 ns | 0.0476 ns | 0.7728 ns | 0.6898 ns | 0.8455 ns | ? | ? | - | - | ? | +| 'Property access - Configuration' | .NET 9.0 | 0.7484 ns | 0.0522 ns | 0.0489 ns | 0.7393 ns | 0.6860 ns | 0.8567 ns | ? | ? | - | - | ? | +| | | | | | | | | | | | | | +| 'Property modification - set all properties' | .NET 10.0 | 2,059.4134 ns | 54.0381 ns | 62.2303 ns | 2,050.0560 ns | 1,950.9536 ns | 2,162.7347 ns | ? | ? | 0.2571 | 4120 B | ? | +| | | | | | | | | | | | | | +| 'Property modification - set all properties' | .NET 9.0 | 3,025.8012 ns | 65.7136 ns | 73.0405 ns | 3,030.1731 ns | 2,919.6998 ns | 3,167.3712 ns | ? | ? | 0.2567 | 4120 B | ? | +| | | | | | | | | | | | | | +| 'ValidateOptions - valid state' | .NET 10.0 | 6.6174 ns | 0.1017 ns | 0.0951 ns | 6.5929 ns | 6.5174 ns | 6.8342 ns | ? | ? | - | - | ? | +| | | | | | | | | | | | | | +| 'ValidateOptions - valid state' | .NET 9.0 | 3.8764 ns | 0.0950 ns | 0.0933 ns | 3.8613 ns | 3.7397 ns | 4.0996 ns | ? | ? | - | - | ? | From a65dccec118eebf5caa47e664ae94e390fa05f4b Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 9 Dec 2025 01:08:13 +0100 Subject: [PATCH 06/32] :lock: refactor assembly resolution for thread safety and speed --- .../BenchmarkWorkspace.cs | 107 +++++++++++------- .../BenchmarkWorkspaceOptionsTest.cs | 2 +- 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs index 74fd85c..d4d0263 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Threading; namespace Codebelt.Extensions.BenchmarkDotNet; @@ -17,6 +18,8 @@ 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); /// /// Initializes a new instance of the class with the specified options. @@ -79,55 +82,29 @@ public void PostProcessArtifacts() private static IEnumerable LoadAssemblies(string repositoryPath, string targetFrameworkMoniker, string benchmarkProjectSuffix, string repositoryTuningFolder, bool useDebugBuild) { var tuningDir = Path.Combine(repositoryPath, repositoryTuningFolder); + Directory.CreateDirectory(tuningDir); - if (!Directory.Exists(tuningDir)) - { - Directory.CreateDirectory(tuningDir); - } + UpdateAssemblyLookup(tuningDir); + EnsureAssemblyResolverRegistered(); - if (!_assemblyResolverRegistered) - { - AppDomain.CurrentDomain.AssemblyResolve += (_, args) => - { - try - { - var requestedName = new AssemblyName(args.Name).Name; - if (string.IsNullOrEmpty(requestedName)) { return null; } - var fileName = requestedName + ".dll"; - - var match = Directory.EnumerateFiles(tuningDir, fileName, SearchOption.AllDirectories).FirstOrDefault(); - if (match != null) - { - var asmName = AssemblyName.GetAssemblyName(match); - var already = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), asmName)); - if (already != null) { return already; } - return Assembly.LoadFrom(match); - } - } - catch - { - // swallow and allow default resolution to continue - } - return null; - }; - _assemblyResolverRegistered = true; - } + var debugOrRelease = useDebugBuild ? "Debug" : "Release"; + var buildSegment = Path.Combine("bin", debugOrRelease, targetFrameworkMoniker); var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies(); - var assemblies = new List(); - var filteredAssemblies = Directory.EnumerateFiles(tuningDir, $"*.{benchmarkProjectSuffix}.dll", SearchOption.AllDirectories); - var debugOrRelease = useDebugBuild - ? "Debug" - : "Release"; - foreach (var path in filteredAssemblies.Where(path => path.Contains($"bin{Path.DirectorySeparatorChar}{debugOrRelease}{Path.DirectorySeparatorChar}{targetFrameworkMoniker}", StringComparison.OrdinalIgnoreCase))) + var candidatePaths = Directory + .EnumerateFiles(tuningDir, $"*.{benchmarkProjectSuffix}.dll", SearchOption.AllDirectories) + .Where(path => path.IndexOf(buildSegment, StringComparison.OrdinalIgnoreCase) >= 0); + + foreach (var path in candidatePaths) { try { var candidateName = AssemblyName.GetAssemblyName(path); - var existing = alreadyLoaded.FirstOrDefault(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName)); + var existing = alreadyLoaded.FirstOrDefault(a => + AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName)); + if (existing != null) { assemblies.Add(existing); @@ -143,7 +120,7 @@ private static IEnumerable LoadAssemblies(string repositoryPath, strin } catch { - // intentionally swallow to allow other assemblies to load + // swallow and continue } } @@ -155,6 +132,56 @@ private static IEnumerable LoadAssemblies(string repositoryPath, strin 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; } diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs index 2199328..afa9657 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs @@ -1,4 +1,4 @@ -using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Configs; using Codebelt.Extensions.Xunit; using System; using System.IO; From ed120db6963c54a10bc05cdf0534266bbc6c5544 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 9 Dec 2025 01:08:56 +0100 Subject: [PATCH 07/32] :memo: update ai docs for this repository --- .github/copilot-instructions.md | 50 ++++----- .github/prompts/benchmark.prompt.md | 165 ++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 25 deletions(-) create mode 100644 .github/prompts/benchmark.prompt.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 12b771e..9ef3035 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,10 +1,10 @@ --- -description: 'Writing Unit Tests in ClassLibrary1' +description: 'Writing Unit Tests in Codebelt.Extensions.BenchmarkDotNet' applyTo: "**/*.{cs,csproj}" --- -# Writing Unit Tests in ClassLibrary1 -This document provides instructions for writing unit tests in the ClassLibrary1 codebase. Please follow these guidelines to ensure consistency and maintainability. +# Writing Unit Tests in Codebelt.Extensions.BenchmarkDotNet +This document provides instructions for writing unit tests in the Codebelt.Extensions.BenchmarkDotNet codebase. Please follow these guidelines to ensure consistency and maintainability. ## 1. Base Class @@ -48,33 +48,33 @@ namespace Your.Namespace ## 5. File and Namespace Organization - Place test files in the appropriate test project and folder structure. -- Use namespaces that mirror the source code structure. The namespace of a test file MUST match the namespace of the System Under Test (SUT). Do NOT append ".Tests", ".Benchmarks" or similar suffixes to the namespace. Only the assembly/project name should indicate that the file is a test/benchmark (for example: ClassLibrary1.Foo.Tests assembly, but namespace ClassLibrary1.Foo). +- Use namespaces that mirror the source code structure. The namespace of a test file MUST match the namespace of the System Under Test (SUT). Do NOT append ".Tests", ".Benchmarks" or similar suffixes to the namespace. Only the assembly/project name should indicate that the file is a test/benchmark (for example: Codebelt.Extensions.BenchmarkDotNet.Foo.Tests assembly, but namespace Codebelt.Extensions.BenchmarkDotNet.Foo). - Example: If the SUT class is declared as: ```csharp - namespace ClassLibrary1.Foo.Bar + namespace Codebelt.Extensions.BenchmarkDotNet.Foo.Bar { public class Zoo { /* ... */ } } ``` then the corresponding unit test class must use the exact same namespace: ```csharp - namespace ClassLibrary1.Foo.Bar + namespace Codebelt.Extensions.BenchmarkDotNet.Foo.Bar { public class ZooTest : Test { /* ... */ } } ``` - Do NOT use: ```csharp - namespace ClassLibrary1.Foo.Bar.Tests { /* ... */ } // ❌ - namespace ClassLibrary1.Foo.Bar.Benchmarks { /* ... */ } // ❌ + namespace Codebelt.Extensions.BenchmarkDotNet.Foo.Bar.Tests { /* ... */ } // ❌ + namespace Codebelt.Extensions.BenchmarkDotNet.Foo.Bar.Benchmarks { /* ... */ } // ❌ ``` -- The unit tests for the ClassLibrary1.Foo assembly live in the ClassLibrary1.Foo.Tests assembly. -- The functional tests for the ClassLibrary1.Foo assembly live in the ClassLibrary1.Foo.FunctionalTests assembly. -- Test class names end with Test and live in the same namespace as the class being tested, e.g., the unit tests for the Boo class that resides in the ClassLibrary1.Foo assembly would be named BooTest and placed in the ClassLibrary1.Foo namespace in the ClassLibrary1.Foo.Tests assembly. +- The unit tests for the Codebelt.Extensions.BenchmarkDotNet.Foo assembly live in the Codebelt.Extensions.BenchmarkDotNet.Foo.Tests assembly. +- The functional tests for the Codebelt.Extensions.BenchmarkDotNet.Foo assembly live in the Codebelt.Extensions.BenchmarkDotNet.Foo.FunctionalTests assembly. +- Test class names end with Test and live in the same namespace as the class being tested, e.g., the unit tests for the Boo class that resides in the Codebelt.Extensions.BenchmarkDotNet.Foo assembly would be named BooTest and placed in the Codebelt.Extensions.BenchmarkDotNet.Foo namespace in the Codebelt.Extensions.BenchmarkDotNet.Foo.Tests assembly. - Modify the associated .csproj file to override the root namespace so the compiled namespace matches the SUT. Example: ```xml - ClassLibrary1.Foo + Codebelt.Extensions.BenchmarkDotNet.Foo ``` - When generating test scaffolding automatically, resolve the SUT's namespace from the source file (or project/assembly metadata) and use that exact namespace in the test file header. @@ -91,7 +91,7 @@ using System.Globalization; using Codebelt.Extensions.Xunit; using Xunit; -namespace ClassLibrary1 +namespace Codebelt.Extensions.BenchmarkDotNet { /// /// Tests for the class. @@ -150,29 +150,29 @@ namespace ClassLibrary1 - Never mock IMarshaller; always use a new instance of JsonMarshaller. --- -description: 'Writing Performance Tests in ClassLibrary1' +description: 'Writing Performance Tests in Codebelt.Extensions.BenchmarkDotNet' applyTo: "tuning/**, **/*Benchmark*.cs" --- -# Writing Performance Tests in ClassLibrary1 -This document provides guidance for writing performance tests (benchmarks) in the ClassLibrary1 codebase using BenchmarkDotNet. Follow these guidelines to keep benchmarks consistent, readable, and comparable. +# Writing Performance Tests in Codebelt.Extensions.BenchmarkDotNet +This document provides guidance for writing performance tests (benchmarks) in the Codebelt.Extensions.BenchmarkDotNet codebase using BenchmarkDotNet. Follow these guidelines to keep benchmarks consistent, readable, and comparable. ## 1. Naming and Placement - Place micro- and component-benchmarks under the `tuning/` folder or in projects named `*.Benchmarks`. - Place benchmark files in the appropriate benchmark project and folder structure. - Use namespaces that mirror the source code structure, e.g. do not suffix with `Benchmarks`. -- Namespace rule: DO NOT append `.Benchmarks` to the namespace. Benchmarks must live in the same namespace as the production assembly. Example: if the production assembly uses `namespace ClassLibrary1.Security.Cryptography`, the benchmark file should also use: +- Namespace rule: DO NOT append `.Benchmarks` to the namespace. Benchmarks must live in the same namespace as the production assembly. Example: if the production assembly uses `namespace Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography`, the benchmark file should also use: ``` - namespace ClassLibrary1.Security.Cryptography + namespace Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography { public class Sha512256Benchmark { /* ... */ } } ``` -The class name may end with `Benchmark`, but the namespace must match the assembly (no `.Benchmarks` suffix). -- The benchmarks for the ClassLibrary1.Bar assembly live in the ClassLibrary1.Bar.Benchmarks assembly. -- Benchmark class names end with Benchmark and live in the same namespace as the class being measured, e.g., the benchmarks for the Zoo class that resides in the ClassLibrary1.Bar assembly would be named ZooBenchmark and placed in the ClassLibrary1.Bar namespace in the ClassLibrary1.Bar.Benchmarks assembly. -- Modify the associated .csproj file to override the root namespace, e.g., ClassLibrary1.Bar. +The class name must end with `Benchmark`, but the namespace must match the assembly (no `.Benchmarks` suffix). +- The benchmarks for the Codebelt.Extensions.BenchmarkDotNet.Bar assembly live in the Codebelt.Extensions.BenchmarkDotNet.Bar.Benchmarks assembly. +- Benchmark class names end with Benchmark and live in the same namespace as the class being measured, e.g., the benchmarks for the Zoo class that resides in the Codebelt.Extensions.BenchmarkDotNet.Bar assembly would be named ZooBenchmark and placed in the Codebelt.Extensions.BenchmarkDotNet.Bar namespace in the Codebelt.Extensions.BenchmarkDotNet.Bar.Benchmarks assembly. +- Modify the associated .csproj file to override the root namespace, e.g., Codebelt.Extensions.BenchmarkDotNet.Bar. ## 2. Attributes and Configuration @@ -203,7 +203,7 @@ The class name may end with `Benchmark`, but the namespace must match the assemb using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; -namespace ClassLibrary1 +namespace Codebelt.Extensions.BenchmarkDotNet { [MemoryDiagnoser] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] @@ -244,11 +244,11 @@ namespace ClassLibrary1 For further examples, refer to the benchmark files under the `tuning/` folder. --- -description: 'Writing XML documentation in ClassLibrary1' +description: 'Writing XML documentation in Codebelt.Extensions.BenchmarkDotNet' applyTo: "**/*.cs" --- -# Writing XML documentation in ClassLibrary1 +# Writing XML documentation in Codebelt.Extensions.BenchmarkDotNet This document provides instructions for writing XML documentation. ## 1. Documentation Style diff --git a/.github/prompts/benchmark.prompt.md b/.github/prompts/benchmark.prompt.md new file mode 100644 index 0000000..8d2e97b --- /dev/null +++ b/.github/prompts/benchmark.prompt.md @@ -0,0 +1,165 @@ +--- +mode: agent +description: 'Writing Performance Benchmarks in Codebelt.Extensions.BenchmarkDotNet' +--- + +# Benchmark Fixture Prompt (Codebelt.Extensions.BenchmarkDotNet Tuning Benchmarks) + +This prompt defines how to generate performance tests (“benchmarks”) for the Codebelt.Extensions.BenchmarkDotNet codebase using BenchmarkDotNet. +Benchmarks in Codebelt.Extensions.BenchmarkDotNet are *not* unit tests — they are micro- or component-level performance measurements that belong under the `tuning/` directory and follow strict conventions. + +Copilot must follow these guidelines when generating benchmark fixtures. + +--- + +## 1. Naming and Placement + +- All benchmark projects live under the `tuning/` folder. + Examples: + - `tuning/Codebelt.Extensions.BenchmarkDotNet.Core.Benchmarks/` + - `tuning/Codebelt.Extensions.BenchmarkDotNet.Security.Benchmarks/` + +- **Namespaces must NOT end with `.Benchmarks`.** + They must mirror the production assembly’s namespace. + + Example: + If benchmarking a type inside `Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography`, then: + + ```csharp + namespace Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography + { + public class Sha512256Benchmark { … } + } + ``` + +* **Benchmark class names must end with `Benchmark`.** + Example: `DateSpanBenchmark`, `FowlerNollVoBenchmark`. + +* Benchmark files should be located in the matching benchmark project + (e.g., benchmarks for `Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography` go in `Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography.Benchmarks.csproj`). + +* In the `.csproj` for each benchmark project, set the root namespace to the production namespace: + + ```xml + Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography + ``` + +--- + +## 2. Attributes and Configuration + +Each benchmark class should use: + +```csharp +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +``` + +Optional but strongly recommended where meaningful: + +* `[Params(...)]` — define small, medium, large input sizes. +* `[GlobalSetup]` — deterministic initialization of benchmark data. +* `[Benchmark(Description = "...")]` — always add descriptions. +* `[Benchmark(Baseline = true)]` — when comparing two implementations. + +Avoid complex global configs; prefer explicit attributes inside the class. + +--- + +## 3. Structure and Best Practices + +A benchmark fixture must: + +* Measure a **single logical operation** per benchmark method. +* Avoid I/O, networking, disk access, logging, or side effects. +* Avoid expensive setup inside `[Benchmark]` methods. +* Use deterministic data (e.g., seeded RNG or predefined constants). +* Use `[GlobalSetup]` to allocate buffers, random payloads, or reusable test data only once. +* Avoid shared mutable state unless reset per iteration. + +Use representative input sizes such as: + +```csharp +[Params(8, 256, 4096)] +public int Count { get; set; } +``` + +BenchmarkDotNet will run each benchmark for each parameter value. + +--- + +## 4. Method Naming Conventions + +Use descriptive names that communicate intent: + +* `Parse_Short` +* `Parse_Long` +* `ComputeHash_Small` +* `ComputeHash_Large` +* `Serialize_Optimized` +* `Serialize_Baseline` + +When comparing approaches, always list them clearly and tag one as the baseline. + +--- + +## 5. Example Benchmark Fixture + +```csharp +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Codebelt.Extensions.BenchmarkDotNet +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class SampleOperationBenchmark + { + [Params(8, 256, 4096)] + public int Count { get; set; } + + private byte[] _payload; + + [GlobalSetup] + public void Setup() + { + _payload = new byte[Count]; + // deterministic initialization + } + + [Benchmark(Baseline = true, Description = "Operation - baseline")] + public int Operation_Baseline() => SampleOperation.Process(_payload); + + [Benchmark(Description = "Operation - optimized")] + public int Operation_Optimized() => SampleOperation.ProcessOptimized(_payload); + } +} +``` + +--- + +## 6. Reporting and CI + +* Benchmark projects live exclusively under `tuning/`. They must not affect production builds. +* Heavy BenchmarkDotNet runs should *not* run in CI unless explicitly configured. +* Reports are produced by the Codebelt.Extensions.BenchmarkDotNet benchmark runner and stored under its configured artifacts directory. + +--- + +## 7. Additional Guidelines + +* Keep benchmark fixtures focused and readable. +* Document non-obvious reasoning in short comments. +* Prefer realistic but deterministic data sets. +* When benchmarks reveal regressions or improvements, reference the associated PR or issue in a comment. +* Shared benchmark helpers belong in `tuning/` projects, not in production code. + +--- + +## Final Notes + +* Benchmarks are performance tests, not unit tests. +* Use `[Benchmark]` only for pure performance measurement. +* Avoid `MethodImplOptions.NoInlining` unless absolutely necessary. +* Use small sets of meaningful benchmark scenarios — avoid combinatorial explosion. + From 54e3de1edc0fd3eedfcbbb8306ec9302b8804c67 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 9 Dec 2025 01:35:14 +0100 Subject: [PATCH 08/32] :rocket: add console integration for project --- .../BenchmarkContext.cs | 22 + .../BenchmarkProgram.cs | 70 +++ .../BenchmarkWorker.cs | 80 +++ ....Extensions.BenchmarkDotNet.Console.csproj | 11 + .../BenchmarkContextTest.cs | 223 ++++++++ .../BenchmarkProgramTest.cs | 402 +++++++++++++ .../BenchmarkWorkerTest.cs | 539 ++++++++++++++++++ ...sions.BenchmarkDotNet.Console.Tests.csproj | 11 + ...lt.Extensions.BenchmarkDotNet.Tests.csproj | 6 - 9 files changed, 1358 insertions(+), 6 deletions(-) create mode 100644 src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs create mode 100644 src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs create mode 100644 src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs create mode 100644 src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj create mode 100644 test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs create mode 100644 test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs create mode 100644 test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs create mode 100644 test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/Codebelt.Extensions.BenchmarkDotNet.Console.Tests.csproj diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs new file mode 100644 index 0000000..4942782 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs @@ -0,0 +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; } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs new file mode 100644 index 0000000..d42aab9 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs @@ -0,0 +1,70 @@ +using Codebelt.Bootstrapper.Console; +using Cuemon; +using Cuemon.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console +{ + /// + /// Entry point helper for hosting and running benchmarks using BenchmarkDotNet. + /// + /// + public class BenchmarkProgram : ConsoleProgram + { + static BenchmarkProgram() + { + var isDebugBuild = Decorator.Enclose(Assembly.GetEntryAssembly()).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, 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 + { + var hostBuilder = CreateHostBuilder(args); + hostBuilder.ConfigureServices(services => + { + services.AddSingleton(new BenchmarkContext(args)); + services.AddBenchmarkWorkspace(setup); + }); + var host = hostBuilder.Build(); + host.Run(); + } + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs new file mode 100644 index 0000000..e0e0f23 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs @@ -0,0 +1,80 @@ +using BenchmarkDotNet.Running; +using Codebelt.Bootstrapper.Console; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console +{ + /// + /// Worker responsible for executing benchmarks within the console host. + /// + /// + public class BenchmarkWorker : ConsoleStartup + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The host environment. + public BenchmarkWorker(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) + { + } + + /// + /// Configures services for the benchmark runner. Override this method to customize service registration. + /// + /// The service collection to configure. + /// + /// Suppresses console lifetime status messages to keep benchmark output clean. + /// + public override void ConfigureServices(IServiceCollection services) + { + services.Configure(o => o.SuppressStatusMessages = true); + } + + /// + /// Runs the actual benchmarks as envisioned by BenchmarkDotNet. + /// + /// The service provider. + /// The cancellation token. + /// A completed task when benchmark execution has finished. + /// + /// When arguments are provided, they are forwarded to for selective execution. + /// After execution completes, the worker performs artifact post-processing. + /// + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + var options = serviceProvider.GetRequiredService(); + var workspace = serviceProvider.GetRequiredService(); + var assemblies = workspace.LoadBenchmarkAssemblies(); + var context = serviceProvider.GetRequiredService(); + + try + { + if (context.Args.Length == 0) + { + foreach (var assembly in assemblies) + { + BenchmarkRunner.Run(assembly, options.Configuration); + } + } + else + { + BenchmarkSwitcher + .FromAssemblies(assemblies) + .Run(context.Args, options.Configuration); + } + } + finally + { + workspace.PostProcessArtifacts(); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj b/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj new file mode 100644 index 0000000..ae58d14 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs new file mode 100644 index 0000000..5df38f3 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs @@ -0,0 +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"); + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs new file mode 100644 index 0000000..0e52f8d --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs @@ -0,0 +1,402 @@ +using System; +using Codebelt.Extensions.Xunit; +using System.Reflection; +using System.Linq; +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; + } + + // 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; + } + + // 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("ConsoleProgram`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(BenchmarkWorker), 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)); + + TestOutput.WriteLine($"Generic type parameter '{typeParameter.Name}' has correct constraints: class, IBenchmarkWorkspace"); + } + + [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] + 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); + + TestOutput.WriteLine($"Setup parameter is optional with default value: {setupParameter.DefaultValue ?? "null"}"); + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs new file mode 100644 index 0000000..9a4f9f4 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs @@ -0,0 +1,539 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +/// +/// Tests for the class. +/// +public class BenchmarkWorkerTest : Test +{ + public BenchmarkWorkerTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldInitialize_WithValidParameters() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + + // Act + var worker = new BenchmarkWorker(configuration, environment); + + // Assert + Assert.NotNull(worker); + } + + [Fact] + public void Constructor_ShouldAcceptNullConfiguration() + { + // Arrange + var environment = CreateMockHostEnvironment(); + + // Act + var worker = new BenchmarkWorker(null, environment); + + // Assert + Assert.NotNull(worker); + } + + [Fact] + public void Constructor_ShouldAcceptNullEnvironment() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + + // Act + var worker = new BenchmarkWorker(configuration, null); + + // Assert + Assert.NotNull(worker); + } + + [Fact] + public void Constructor_ShouldAcceptBothParametersNull() + { + // Act + var worker = new BenchmarkWorker(null, null); + + // Assert + Assert.NotNull(worker); + } + + [Fact] + public void BenchmarkWorker_ShouldInheritFromConsoleStartup() + { + // Act + var baseType = typeof(BenchmarkWorker).BaseType; + + // Assert + Assert.NotNull(baseType); + Assert.Equal("ConsoleStartup", baseType.Name); + + TestOutput.WriteLine($"BenchmarkWorker correctly inherits from: {baseType.FullName}"); + } + + [Fact] + public void BenchmarkWorker_ShouldBePublicClass() + { + // Act + var type = typeof(BenchmarkWorker); + + // Assert + Assert.True(type.IsPublic); + Assert.True(type.IsClass); + Assert.False(type.IsAbstract); + Assert.False(type.IsSealed); + + TestOutput.WriteLine("BenchmarkWorker is a public, non-abstract, non-sealed class"); + } + + [Fact] + public void ConfigureServices_ShouldExist_AndBePublic() + { + // Act + var method = typeof(BenchmarkWorker).GetMethod("ConfigureServices", BindingFlags.Public | BindingFlags.Instance); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsPublic); + Assert.True(method.IsVirtual); + Assert.Equal(typeof(void), method.ReturnType); + + var parameters = method.GetParameters(); + Assert.Single(parameters); + Assert.Equal(typeof(IServiceCollection), parameters[0].ParameterType); + + TestOutput.WriteLine($"Found ConfigureServices method: {method}"); + } + + [Fact] + public void ConfigureServices_ShouldConfigureConsoleLifetimeOptions() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + var services = new ServiceCollection(); + + // Act + worker.ConfigureServices(services); + + // Assert + var serviceDescriptor = services.FirstOrDefault(s => + s.ServiceType.IsGenericType && + s.ServiceType.GetGenericTypeDefinition() == typeof(IConfigureOptions<>) && + s.ServiceType.GetGenericArguments()[0] == typeof(ConsoleLifetimeOptions)); + + Assert.NotNull(serviceDescriptor); + + TestOutput.WriteLine("ConsoleLifetimeOptions configuration was registered"); + } + + [Fact] + public void ConfigureServices_ShouldSuppressStatusMessages() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + var services = new ServiceCollection(); + + // Act + worker.ConfigureServices(services); + + // Build service provider and resolve options + var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetService>()?.Value; + + // Assert + Assert.NotNull(options); + Assert.True(options.SuppressStatusMessages); + + TestOutput.WriteLine($"ConsoleLifetimeOptions.SuppressStatusMessages = {options.SuppressStatusMessages}"); + } + + [Fact] + public void ConfigureServices_ShouldHandleNullServiceCollection() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + // Act & Assert + Assert.Throws(() => worker.ConfigureServices(null)); + } + + [Fact] + public void RunAsync_ShouldExist_AndBePublic() + { + // Act + var method = typeof(BenchmarkWorker).GetMethod("RunAsync", BindingFlags.Public | BindingFlags.Instance); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsPublic); + Assert.True(method.IsVirtual); + Assert.Equal(typeof(Task), method.ReturnType); + + var parameters = method.GetParameters(); + Assert.Equal(2, parameters.Length); + Assert.Equal(typeof(IServiceProvider), parameters[0].ParameterType); + Assert.Equal(typeof(CancellationToken), parameters[1].ParameterType); + + TestOutput.WriteLine($"Found RunAsync method: {method}"); + } + + [Fact] + public async Task RunAsync_ShouldCompleteSuccessfully_WithValidServiceProvider() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var services = CreateServiceProviderWithMocks(); + var cancellationToken = CancellationToken.None; + + // Act + var task = worker.RunAsync(services, cancellationToken); + + // Assert + Assert.NotNull(task); + await task; + Assert.True(task.IsCompletedSuccessfully); + + TestOutput.WriteLine("RunAsync completed successfully"); + } + + [Fact] + public async Task RunAsync_ShouldReturnCompletedTask() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var services = CreateServiceProviderWithMocks(); + var cancellationToken = CancellationToken.None; + + // Act + var task = worker.RunAsync(services, cancellationToken); + + // Assert + await task; + Assert.Equal(TaskStatus.RanToCompletion, task.Status); + Assert.False(task.IsFaulted); + Assert.False(task.IsCanceled); + + TestOutput.WriteLine($"Task status: {task.Status}"); + } + + [Fact] + public async Task RunAsync_ShouldHandleEmptyArgs() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var services = CreateServiceProviderWithMocks(new string[] { }); + var cancellationToken = CancellationToken.None; + + // Act + var task = worker.RunAsync(services, cancellationToken); + + // Assert + await task; + Assert.True(task.IsCompletedSuccessfully); + + TestOutput.WriteLine("RunAsync handled empty args successfully"); + } + + [Fact] + public async Task RunAsync_ShouldHandleArgsWithValues() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var args = new[] { "--filter", "MyBenchmark", "--job", "short" }; + var services = CreateServiceProviderWithMocks(args); + var cancellationToken = CancellationToken.None; + + // Act + var task = worker.RunAsync(services, cancellationToken); + + // Assert + await task; + Assert.True(task.IsCompletedSuccessfully); + + TestOutput.WriteLine($"RunAsync handled args with {args.Length} values successfully"); + } + + [Fact] + public async Task RunAsync_ShouldCallPostProcessArtifacts() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var workspace = new FakeBenchmarkWorkspace(); + var services = CreateServiceProviderWithMocks(workspace: workspace); + var cancellationToken = CancellationToken.None; + + // Act + await worker.RunAsync(services, cancellationToken); + + // Assert + Assert.True(workspace.PostProcessArtifactsCalled); + + TestOutput.WriteLine("PostProcessArtifacts was called"); + } + + [Fact] + public async Task RunAsync_ShouldCallLoadBenchmarkAssemblies() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var workspace = new FakeBenchmarkWorkspace(); + var services = CreateServiceProviderWithMocks(workspace: workspace); + var cancellationToken = CancellationToken.None; + + // Act + await worker.RunAsync(services, cancellationToken); + + // Assert + Assert.True(workspace.LoadBenchmarkAssembliesCalled); + + TestOutput.WriteLine("LoadBenchmarkAssemblies was called"); + } + + [Fact] + public async Task RunAsync_ShouldHandleCancellationToken() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var services = CreateServiceProviderWithMocks(); + var cts = new CancellationTokenSource(); + + // Act + var task = worker.RunAsync(services, cts.Token); + + // Assert + await task; + Assert.True(task.IsCompletedSuccessfully); + + TestOutput.WriteLine("RunAsync handled cancellation token"); + } + + [Fact] + public async Task RunAsync_ShouldCallPostProcessArtifacts_EvenWithEmptyAssemblies() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var workspace = new FakeBenchmarkWorkspace(Array.Empty()); + var services = CreateServiceProviderWithMocks(workspace: workspace); + var cancellationToken = CancellationToken.None; + + // Act + await worker.RunAsync(services, cancellationToken); + + // Assert + Assert.True(workspace.PostProcessArtifactsCalled); + + TestOutput.WriteLine("PostProcessArtifacts was called even with empty assemblies"); + } + + [Fact] + public void Constructor_ShouldHaveCorrectParameterNames() + { + // Act + var constructor = typeof(BenchmarkWorker).GetConstructors().Single(); + var parameters = constructor.GetParameters(); + + // Assert + Assert.Equal(2, parameters.Length); + Assert.Equal("configuration", parameters[0].Name); + Assert.Equal("environment", parameters[1].Name); + + TestOutput.WriteLine($"Constructor parameters: {string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"))}"); + } + + [Fact] + public void BenchmarkWorker_ShouldHaveXmlDocumentation() + { + // This test verifies that the class has XML documentation + // by checking for the summary element in the XML doc + + // Act + var type = typeof(BenchmarkWorker); + + // Assert + Assert.NotNull(type); + Assert.True(type.IsPublic); + + TestOutput.WriteLine($"BenchmarkWorker type: {type.FullName}"); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(3)] + [InlineData(5)] + public async Task RunAsync_ShouldHandleVariousArgCounts(int argCount) + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var args = new string[argCount]; + for (int i = 0; i < argCount; i++) + { + args[i] = $"arg{i}"; + } + + var services = CreateServiceProviderWithMocks(args); + var cancellationToken = CancellationToken.None; + + // Act + var task = worker.RunAsync(services, cancellationToken); + + // Assert + await task; + Assert.True(task.IsCompletedSuccessfully); + + TestOutput.WriteLine($"RunAsync handled {argCount} arguments successfully"); + } + + [Fact] + public async Task RunAsync_ShouldUseConfiguration_FromOptions() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var customConfig = ManualConfig.CreateEmpty(); + var options = new BenchmarkWorkspaceOptions { Configuration = customConfig }; + var services = CreateServiceProviderWithMocks(options: options); + var cancellationToken = CancellationToken.None; + + // Act + await worker.RunAsync(services, cancellationToken); + + // Assert + Assert.True(true); // If we get here without exception, the configuration was used + + TestOutput.WriteLine("RunAsync used configuration from options"); + } + + [Fact] + public void ConfigureServices_ShouldBeOverridable() + { + // Arrange + var method = typeof(BenchmarkWorker).GetMethod("ConfigureServices"); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsVirtual); + Assert.False(method.IsFinal); + + TestOutput.WriteLine("ConfigureServices is virtual and can be overridden"); + } + + [Fact] + public void RunAsync_ShouldBeOverridable() + { + // Arrange + var method = typeof(BenchmarkWorker).GetMethod("RunAsync"); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsVirtual); + Assert.False(method.IsFinal); + + TestOutput.WriteLine("RunAsync is virtual and can be overridden"); + } + + private static IHostEnvironment CreateMockHostEnvironment() + { + var environment = new FakeHostEnvironment + { + EnvironmentName = "Test", + ApplicationName = "BenchmarkWorkerTest", + ContentRootPath = AppContext.BaseDirectory + }; + return environment; + } + + private static IServiceProvider CreateServiceProviderWithMocks(string[] args = null, BenchmarkWorkspaceOptions options = null, FakeBenchmarkWorkspace workspace = null) + { + var services = new ServiceCollection(); + + // Add required services + services.AddSingleton(options ?? new BenchmarkWorkspaceOptions()); + services.AddSingleton(workspace ?? new FakeBenchmarkWorkspace()); + services.AddSingleton(new BenchmarkContext(args ?? Array.Empty())); + + return services.BuildServiceProvider(); + } + + private class FakeHostEnvironment : IHostEnvironment + { + public string EnvironmentName { get; set; } + public string ApplicationName { get; set; } + public string ContentRootPath { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } + } + + private class FakeBenchmarkWorkspace : IBenchmarkWorkspace + { + private readonly Assembly[] _assemblies; + + public FakeBenchmarkWorkspace(Assembly[] assemblies = null) + { + _assemblies = assemblies ?? new[] { typeof(BenchmarkWorkerTest).Assembly }; + } + + public bool LoadBenchmarkAssembliesCalled { get; private set; } + public bool PostProcessArtifactsCalled { get; private set; } + + public Assembly[] LoadBenchmarkAssemblies() + { + LoadBenchmarkAssembliesCalled = true; + return _assemblies; + } + + public void PostProcessArtifacts() + { + PostProcessArtifactsCalled = true; + } + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/Codebelt.Extensions.BenchmarkDotNet.Console.Tests.csproj b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/Codebelt.Extensions.BenchmarkDotNet.Console.Tests.csproj new file mode 100644 index 0000000..62f42db --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/Codebelt.Extensions.BenchmarkDotNet.Console.Tests.csproj @@ -0,0 +1,11 @@ + + + + Codebelt.Extensions.BenchmarkDotNet.Console + + + + + + + diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj index 28bfdd5..24cde66 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj @@ -4,12 +4,6 @@ Codebelt.Extensions.BenchmarkDotNet - - - false - - - From bda283c7bcd2f47b669993421a731da58998d43c Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 9 Dec 2025 01:35:33 +0100 Subject: [PATCH 09/32] :heavy_plus_sign: add solution file for project organization --- Codebelt.Extensions.BenchmarkDotNet.sln | 71 +++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 Codebelt.Extensions.BenchmarkDotNet.sln diff --git a/Codebelt.Extensions.BenchmarkDotNet.sln b/Codebelt.Extensions.BenchmarkDotNet.sln new file mode 100644 index 0000000..3d47990 --- /dev/null +++ b/Codebelt.Extensions.BenchmarkDotNet.sln @@ -0,0 +1,71 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11222.15 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Codebelt.Extensions.BenchmarkDotNet", "src\Codebelt.Extensions.BenchmarkDotNet\Codebelt.Extensions.BenchmarkDotNet.csproj", "{A9DFF36B-1AD4-40EC-9394-C720C3DC785A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0070E83B-2DDD-4537-A83F-1CF8644F2880}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A3C56B2E-55EE-44EC-876E-B03B8DDA3317}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tooling", "tooling", "{0A82E699-D3C7-4BC0-8E25-AE5279633227}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Extensions.BenchmarkDotNet.Runner", "tooling\Codebelt.Extensions.BenchmarkDotNet.Runner\Codebelt.Extensions.BenchmarkDotNet.Runner.csproj", "{28B11ADB-7AEE-4920-BD8A-73D844486087}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tuning", "tuning", "{FC51D601-926B-4425-96D9-46C32B5C528D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Extensions.BenchmarkDotNet.Benchmarks", "tuning\Codebelt.Extensions.BenchmarkDotNet.Benchmarks\Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj", "{74B646AA-484C-4780-A15D-0F90DDC7B0DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Extensions.BenchmarkDotNet.Tests", "test\Codebelt.Extensions.BenchmarkDotNet.Tests\Codebelt.Extensions.BenchmarkDotNet.Tests.csproj", "{14C806C0-046E-45F1-BCA4-744EEBF87B07}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Extensions.BenchmarkDotNet.Console", "src\Codebelt.Extensions.BenchmarkDotNet.Console\Codebelt.Extensions.BenchmarkDotNet.Console.csproj", "{F1C4A673-F29E-B44F-9655-8E666FD1A520}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Extensions.BenchmarkDotNet.Console.Tests", "test\Codebelt.Extensions.BenchmarkDotNet.Console.Tests\Codebelt.Extensions.BenchmarkDotNet.Console.Tests.csproj", "{20623AAB-3B2D-449F-918E-71125E22ECDA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Release|Any CPU.Build.0 = Release|Any CPU + {28B11ADB-7AEE-4920-BD8A-73D844486087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28B11ADB-7AEE-4920-BD8A-73D844486087}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28B11ADB-7AEE-4920-BD8A-73D844486087}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28B11ADB-7AEE-4920-BD8A-73D844486087}.Release|Any CPU.Build.0 = Release|Any CPU + {74B646AA-484C-4780-A15D-0F90DDC7B0DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74B646AA-484C-4780-A15D-0F90DDC7B0DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74B646AA-484C-4780-A15D-0F90DDC7B0DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74B646AA-484C-4780-A15D-0F90DDC7B0DC}.Release|Any CPU.Build.0 = Release|Any CPU + {14C806C0-046E-45F1-BCA4-744EEBF87B07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14C806C0-046E-45F1-BCA4-744EEBF87B07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14C806C0-046E-45F1-BCA4-744EEBF87B07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14C806C0-046E-45F1-BCA4-744EEBF87B07}.Release|Any CPU.Build.0 = Release|Any CPU + {F1C4A673-F29E-B44F-9655-8E666FD1A520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1C4A673-F29E-B44F-9655-8E666FD1A520}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1C4A673-F29E-B44F-9655-8E666FD1A520}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1C4A673-F29E-B44F-9655-8E666FD1A520}.Release|Any CPU.Build.0 = Release|Any CPU + {20623AAB-3B2D-449F-918E-71125E22ECDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20623AAB-3B2D-449F-918E-71125E22ECDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20623AAB-3B2D-449F-918E-71125E22ECDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20623AAB-3B2D-449F-918E-71125E22ECDA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A9DFF36B-1AD4-40EC-9394-C720C3DC785A} = {0070E83B-2DDD-4537-A83F-1CF8644F2880} + {28B11ADB-7AEE-4920-BD8A-73D844486087} = {0A82E699-D3C7-4BC0-8E25-AE5279633227} + {74B646AA-484C-4780-A15D-0F90DDC7B0DC} = {FC51D601-926B-4425-96D9-46C32B5C528D} + {14C806C0-046E-45F1-BCA4-744EEBF87B07} = {A3C56B2E-55EE-44EC-876E-B03B8DDA3317} + {F1C4A673-F29E-B44F-9655-8E666FD1A520} = {0070E83B-2DDD-4537-A83F-1CF8644F2880} + {20623AAB-3B2D-449F-918E-71125E22ECDA} = {A3C56B2E-55EE-44EC-876E-B03B8DDA3317} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0CBE2805-F0FF-4D0F-902C-8B9277A5D3F2} + EndGlobalSection +EndGlobal From 5d35470ca3c26f1a10ed30119c6abdfaf258a76a Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 9 Dec 2025 01:36:09 +0100 Subject: [PATCH 10/32] :recycle: update branding, frameworks, and benchmark build settings --- Directory.Build.props | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 933c608..1516ab1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,4 @@ - $(MSBuildProjectName.EndsWith('Tests')) $(MSBuildProjectName.EndsWith('Benchmarks')) @@ -9,6 +8,7 @@ $([MSBuild]::IsOSPlatform('Windows')) true false + ..\..\.nuget\$(MSBuildProjectName)\PackageReleaseNotes.txt latest @@ -18,16 +18,16 @@ - net10.0 - Copyright © ClassLibrary1 2025. All rights reserved. - ClassLibrary1 - ClassLibrary1 - ClassLibrary1 + net10.0;net9.0 + Copyright © Geekle 2025. All rights reserved. + gimlichael + Geekle + Extensions for BenchmarkDotNet API by Codebelt icon.png README.md - https://www.classlibrary1.net/ + https://benchmarkdotnet.codebelt.net/ MIT - https://github.com/classlibrary1/ClassLibrary1 + https://github.com/codebeltnet/benchmarkdotnet git en-US true @@ -36,7 +36,7 @@ snupkg true true - $(MSBuildThisFileDirectory)ClassLibrary1.snk + $(MSBuildThisFileDirectory)benchmarkdotnet.snk true latest Recommended @@ -46,7 +46,7 @@ - + @@ -55,15 +55,8 @@ - - net10.0 - - - - net10.0 - - + net10.0;net9.0 false Exe false @@ -107,12 +100,20 @@ - net10.0 + net10.0;net9.0 + false + false + false + false + true + none + NU1701,NU1902,NU1903 + false + false - From a1cf0153cbb3ccd4bd698c1bda83960ed488009d Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 9 Dec 2025 01:36:50 +0100 Subject: [PATCH 11/32] :wastebasket: remove release notes file prop, add clean for benchmark output --- Directory.Build.targets | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 05374a0..f2f12ec 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,8 +1,4 @@  - - ..\..\.nuget\$(MSBuildProjectName)\PackageReleaseNotes.txt - - @@ -18,4 +14,8 @@ $(MinVerMajor).$(MinVerMinor).$(MinVerPatch).$(GITHUB_RUN_NUMBER) + + + + From a3a06283bbc2776fedf750a98d7a0bfe9ba308b6 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 9 Dec 2025 01:37:31 +0100 Subject: [PATCH 12/32] :arrow_up: bump dependencies --- Directory.Packages.props | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 536d017..6070e7f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,8 +5,10 @@ - - + + + + From 42167d2d67eff420d31af26b34abcd6b768da34b Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Thu, 11 Dec 2025 21:12:30 +0100 Subject: [PATCH 13/32] :sparkles: add benchmark suite for core classes and operations --- .../BenchmarkContextBenchmark.cs | 74 +++++++++++++++ .../BenchmarkProgramBenchmark.cs | 61 ++++++++++++ .../BenchmarkWorkerBenchmark.cs | 92 +++++++++++++++++++ ....BenchmarkDotNet.Console.Benchmarks.csproj | 11 +++ 4 files changed, 238 insertions(+) create mode 100644 tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs create mode 100644 tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs create mode 100644 tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkWorkerBenchmark.cs create mode 100644 tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs new file mode 100644 index 0000000..06b45fc --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs @@ -0,0 +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; + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs new file mode 100644 index 0000000..de17aeb --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs @@ -0,0 +1,61 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using Cuemon; +using Cuemon.Reflection; +using System; +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); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkWorkerBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkWorkerBenchmark.cs new file mode 100644 index 0000000..6c0e214 --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkWorkerBenchmark.cs @@ -0,0 +1,92 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using System.Collections.Generic; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +/// +/// Benchmarks for the class. +/// +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkWorkerBenchmark +{ + private IConfiguration _configuration; + private IHostEnvironment _environment; + private BenchmarkWorker _worker; + private IServiceCollection _services; + + [GlobalSetup] + public void Setup() + { + // Deterministic initialization of test data + var configData = new Dictionary + { + ["Setting1"] = "Value1", + ["Setting2"] = "Value2" + }; + + _configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configData) + .Build(); + + _environment = new TestHostEnvironment + { + EnvironmentName = "Development", + ApplicationName = "BenchmarkTest" + }; + + _worker = new BenchmarkWorker(_configuration, _environment); + _services = new ServiceCollection(); + } + + [Benchmark(Baseline = true, Description = "Construct BenchmarkWorker")] + public BenchmarkWorker ConstructWorker() + { + return new BenchmarkWorker(_configuration, _environment); + } + + [Benchmark(Description = "Configure services")] + public IServiceCollection ConfigureServices() + { + var services = new ServiceCollection(); + _worker.ConfigureServices(services); + return services; + } + + [Benchmark(Description = "Configure services with options")] + public IServiceCollection ConfigureServicesWithOptions() + { + var services = new ServiceCollection(); + _worker.ConfigureServices(services); + var provider = services.BuildServiceProvider(); + return services; + } + + [Benchmark(Description = "Access configuration")] + public IConfiguration AccessConfiguration() + { + return _configuration; + } + + [Benchmark(Description = "Access environment")] + public IHostEnvironment AccessEnvironment() + { + return _environment; + } + + /// + /// Test implementation of IHostEnvironment for benchmark purposes. + /// + private class TestHostEnvironment : IHostEnvironment + { + public string EnvironmentName { get; set; } + public string ApplicationName { get; set; } + public string ContentRootPath { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj new file mode 100644 index 0000000..ec5743f --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj @@ -0,0 +1,11 @@ + + + + Codebelt.Extensions.BenchmarkDotNet.Console + + + + + + + From e37b15d1446ef30805e3ea0a564a04997831f079 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Thu, 11 Dec 2025 21:12:59 +0100 Subject: [PATCH 14/32] :recycle: migrate to .slnx format --- Codebelt.Extensions.BenchmarkDotNet.sln | 71 ------------------------ Codebelt.Extensions.BenchmarkDotNet.slnx | 17 ++++++ 2 files changed, 17 insertions(+), 71 deletions(-) delete mode 100644 Codebelt.Extensions.BenchmarkDotNet.sln create mode 100644 Codebelt.Extensions.BenchmarkDotNet.slnx diff --git a/Codebelt.Extensions.BenchmarkDotNet.sln b/Codebelt.Extensions.BenchmarkDotNet.sln deleted file mode 100644 index 3d47990..0000000 --- a/Codebelt.Extensions.BenchmarkDotNet.sln +++ /dev/null @@ -1,71 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.0.11222.15 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Codebelt.Extensions.BenchmarkDotNet", "src\Codebelt.Extensions.BenchmarkDotNet\Codebelt.Extensions.BenchmarkDotNet.csproj", "{A9DFF36B-1AD4-40EC-9394-C720C3DC785A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0070E83B-2DDD-4537-A83F-1CF8644F2880}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A3C56B2E-55EE-44EC-876E-B03B8DDA3317}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tooling", "tooling", "{0A82E699-D3C7-4BC0-8E25-AE5279633227}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Extensions.BenchmarkDotNet.Runner", "tooling\Codebelt.Extensions.BenchmarkDotNet.Runner\Codebelt.Extensions.BenchmarkDotNet.Runner.csproj", "{28B11ADB-7AEE-4920-BD8A-73D844486087}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tuning", "tuning", "{FC51D601-926B-4425-96D9-46C32B5C528D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Extensions.BenchmarkDotNet.Benchmarks", "tuning\Codebelt.Extensions.BenchmarkDotNet.Benchmarks\Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj", "{74B646AA-484C-4780-A15D-0F90DDC7B0DC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Extensions.BenchmarkDotNet.Tests", "test\Codebelt.Extensions.BenchmarkDotNet.Tests\Codebelt.Extensions.BenchmarkDotNet.Tests.csproj", "{14C806C0-046E-45F1-BCA4-744EEBF87B07}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Extensions.BenchmarkDotNet.Console", "src\Codebelt.Extensions.BenchmarkDotNet.Console\Codebelt.Extensions.BenchmarkDotNet.Console.csproj", "{F1C4A673-F29E-B44F-9655-8E666FD1A520}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Extensions.BenchmarkDotNet.Console.Tests", "test\Codebelt.Extensions.BenchmarkDotNet.Console.Tests\Codebelt.Extensions.BenchmarkDotNet.Console.Tests.csproj", "{20623AAB-3B2D-449F-918E-71125E22ECDA}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Release|Any CPU.Build.0 = Release|Any CPU - {28B11ADB-7AEE-4920-BD8A-73D844486087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {28B11ADB-7AEE-4920-BD8A-73D844486087}.Debug|Any CPU.Build.0 = Debug|Any CPU - {28B11ADB-7AEE-4920-BD8A-73D844486087}.Release|Any CPU.ActiveCfg = Release|Any CPU - {28B11ADB-7AEE-4920-BD8A-73D844486087}.Release|Any CPU.Build.0 = Release|Any CPU - {74B646AA-484C-4780-A15D-0F90DDC7B0DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {74B646AA-484C-4780-A15D-0F90DDC7B0DC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {74B646AA-484C-4780-A15D-0F90DDC7B0DC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {74B646AA-484C-4780-A15D-0F90DDC7B0DC}.Release|Any CPU.Build.0 = Release|Any CPU - {14C806C0-046E-45F1-BCA4-744EEBF87B07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14C806C0-046E-45F1-BCA4-744EEBF87B07}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14C806C0-046E-45F1-BCA4-744EEBF87B07}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14C806C0-046E-45F1-BCA4-744EEBF87B07}.Release|Any CPU.Build.0 = Release|Any CPU - {F1C4A673-F29E-B44F-9655-8E666FD1A520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F1C4A673-F29E-B44F-9655-8E666FD1A520}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F1C4A673-F29E-B44F-9655-8E666FD1A520}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F1C4A673-F29E-B44F-9655-8E666FD1A520}.Release|Any CPU.Build.0 = Release|Any CPU - {20623AAB-3B2D-449F-918E-71125E22ECDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20623AAB-3B2D-449F-918E-71125E22ECDA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20623AAB-3B2D-449F-918E-71125E22ECDA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20623AAB-3B2D-449F-918E-71125E22ECDA}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A} = {0070E83B-2DDD-4537-A83F-1CF8644F2880} - {28B11ADB-7AEE-4920-BD8A-73D844486087} = {0A82E699-D3C7-4BC0-8E25-AE5279633227} - {74B646AA-484C-4780-A15D-0F90DDC7B0DC} = {FC51D601-926B-4425-96D9-46C32B5C528D} - {14C806C0-046E-45F1-BCA4-744EEBF87B07} = {A3C56B2E-55EE-44EC-876E-B03B8DDA3317} - {F1C4A673-F29E-B44F-9655-8E666FD1A520} = {0070E83B-2DDD-4537-A83F-1CF8644F2880} - {20623AAB-3B2D-449F-918E-71125E22ECDA} = {A3C56B2E-55EE-44EC-876E-B03B8DDA3317} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {0CBE2805-F0FF-4D0F-902C-8B9277A5D3F2} - EndGlobalSection -EndGlobal diff --git a/Codebelt.Extensions.BenchmarkDotNet.slnx b/Codebelt.Extensions.BenchmarkDotNet.slnx new file mode 100644 index 0000000..bc97d92 --- /dev/null +++ b/Codebelt.Extensions.BenchmarkDotNet.slnx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + From 7eaaa7f28f897a5948b506c6be7e83a9e53f723f Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Thu, 11 Dec 2025 21:13:43 +0100 Subject: [PATCH 15/32] :arrow_up: bump dependencies --- Directory.Packages.props | 4 ++-- testenvironments.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6070e7f..e454e68 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,8 +3,8 @@ true - - + + diff --git a/testenvironments.json b/testenvironments.json index 2bc34cb..9807401 100644 --- a/testenvironments.json +++ b/testenvironments.json @@ -9,7 +9,7 @@ { "name": "Docker-Ubuntu", "type": "docker", - "dockerImage": "gimlichael/ubuntu-testrunner:net8.0.416-9.0.307-10.0.100" + "dockerImage": "gimlichael/ubuntu-testrunner:net8.0.416-9.0.307-10.0.101" } ] } From f8815ad10eeb3ddad73bb34af47d213bde680a87 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Thu, 11 Dec 2025 21:14:11 +0100 Subject: [PATCH 16/32] :memo: update benchmark naming and placement guidelines --- .github/prompts/benchmark.prompt.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/prompts/benchmark.prompt.md b/.github/prompts/benchmark.prompt.md index 8d2e97b..b37a123 100644 --- a/.github/prompts/benchmark.prompt.md +++ b/.github/prompts/benchmark.prompt.md @@ -16,17 +16,17 @@ Copilot must follow these guidelines when generating benchmark fixtures. - All benchmark projects live under the `tuning/` folder. Examples: - - `tuning/Codebelt.Extensions.BenchmarkDotNet.Core.Benchmarks/` - - `tuning/Codebelt.Extensions.BenchmarkDotNet.Security.Benchmarks/` + - `tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/` + - `tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/` - **Namespaces must NOT end with `.Benchmarks`.** They must mirror the production assembly’s namespace. Example: - If benchmarking a type inside `Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography`, then: + If benchmarking a type inside `Codebelt.Extensions.BenchmarkDotNet.Console`, then: ```csharp - namespace Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography + namespace Codebelt.Extensions.BenchmarkDotNet.Console { public class Sha512256Benchmark { … } } @@ -36,12 +36,12 @@ Copilot must follow these guidelines when generating benchmark fixtures. Example: `DateSpanBenchmark`, `FowlerNollVoBenchmark`. * Benchmark files should be located in the matching benchmark project - (e.g., benchmarks for `Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography` go in `Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography.Benchmarks.csproj`). + (e.g., benchmarks for `Codebelt.Extensions.BenchmarkDotNet.Console` go in `Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj`). * In the `.csproj` for each benchmark project, set the root namespace to the production namespace: ```xml - Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography + Codebelt.Extensions.BenchmarkDotNet.Console ``` --- @@ -162,4 +162,3 @@ namespace Codebelt.Extensions.BenchmarkDotNet * Use `[Benchmark]` only for pure performance measurement. * Avoid `MethodImplOptions.NoInlining` unless absolutely necessary. * Use small sets of meaningful benchmark scenarios — avoid combinatorial explosion. - From 3d430f7447a3f6a01772624bc3b7c9645539e5b3 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Thu, 11 Dec 2025 21:47:39 +0100 Subject: [PATCH 17/32] :construction_worker: update ci pipeline for BenchmarkDotNet --- .github/workflows/ci-pipeline.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 6229782..2f4b766 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -1,4 +1,4 @@ -name: ClassLibrary1 CI Pipeline +name: BenchmarkDotNet CI Pipeline on: pull_request: branches: [main] @@ -25,7 +25,7 @@ jobs: uses: codebeltnet/jobs-dotnet-build/.github/workflows/default.yml@v3 with: configuration: ${{ matrix.configuration }} - strong-name-key-filename: classlibrary1.snk + strong-name-key-filename: benchmarkdotnet.snk secrets: inherit pack: @@ -52,14 +52,16 @@ jobs: configuration: ${{ matrix.configuration }} runs-on: ${{ matrix.os }} build-switches: -p:SkipSignAssembly=true + build: true # we need to build due to xUnitv3 + restore: true # we need to restore due to xUnitv3 sonarcloud: name: call-sonarcloud needs: [build, test] uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 with: - organization: yourorg - projectKey: classlibrary1 + organization: geekle + projectKey: bemchmarkdotnet version: ${{ needs.build.outputs.version }} secrets: inherit @@ -68,7 +70,7 @@ jobs: needs: [build, test] uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 with: - repository: yourorg/classlibrary1 + repository: codebeltnet/bemchmarkdotnet secrets: inherit codeql: From bccb3d431308edf8c5ac5dbf39148431229b8669 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Thu, 11 Dec 2025 21:48:03 +0100 Subject: [PATCH 18/32] :fire: remove template release notes, readme, and icon from project --- .nuget/ClassLibrary1/PackageReleaseNotes.txt | 9 --------- .nuget/ClassLibrary1/README.md | 5 ----- .nuget/ClassLibrary1/icon.png | Bin 4131 -> 0 bytes 3 files changed, 14 deletions(-) delete mode 100644 .nuget/ClassLibrary1/PackageReleaseNotes.txt delete mode 100644 .nuget/ClassLibrary1/README.md delete mode 100644 .nuget/ClassLibrary1/icon.png diff --git a/.nuget/ClassLibrary1/PackageReleaseNotes.txt b/.nuget/ClassLibrary1/PackageReleaseNotes.txt deleted file mode 100644 index e2457b4..0000000 --- a/.nuget/ClassLibrary1/PackageReleaseNotes.txt +++ /dev/null @@ -1,9 +0,0 @@ -Version: 0.1.0 -Availability: .NET 10 -  -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) -  -# New Features -- ADDED -  \ No newline at end of file diff --git a/.nuget/ClassLibrary1/README.md b/.nuget/ClassLibrary1/README.md deleted file mode 100644 index 26e0ffa..0000000 --- a/.nuget/ClassLibrary1/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ClassLibrary1 - -An open-source project (MIT license) offering the next great thing withing .NET development. - -Essential code for your ever growing toolbelt of code. diff --git a/.nuget/ClassLibrary1/icon.png b/.nuget/ClassLibrary1/icon.png deleted file mode 100644 index ade21ebd9558869cc07d61eb239ec39e128c373d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4131 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 From f9940f5a28526d0bea80cd904a9455915b2e115a Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Thu, 11 Dec 2025 22:24:04 +0100 Subject: [PATCH 19/32] :bookmark: update changelog for initial stable release of packages --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0eac06..f41a179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. -## [Unreleased] - TBD +## [1.0.0] - 2025-12-12 + +This is the initial stable release of the `Codebelt.Extensions.BenchmarkDotNet` and `Codebelt.Extensions.BenchmarkDotNet.Console` packages. ### Added + +- ADDED `BenchmarkWorkspace` class in the Codebelt.Extensions.BenchmarkDotNet namespace that provides a default implementation of `IBenchmarkWorkspace` for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet, +- ADDED `BenchmarkWorkspaceOptions` class in the Codebelt.Extensions.BenchmarkDotNet namespace that specifies configuration options that is related to the `BenchmarkWorkspace` class, +- ADDED `BenchmarkWorkspaceOptionsExtensions` class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the `BenchmarkWorkspaceOptions` class: `ConfigureBenchmarkDotNet`, +- ADDED `IBenchmarkWorkspace` interface in the Codebelt.Extensions.BenchmarkDotNet namespace that defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet, +- ADDED `ServiceCollectionExtensions` class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the IServiceCollection interface: `AddBenchmarkWorkspace` and `AddBenchmarkWorkspace{TWorkspace}`, +- ADDED `BenchmarkContext` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that represents the command-line context for a benchmark run, +- ADDED `BenchmarkProgram` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that provides the main entry point for hosting and running benchmarks using BenchmarkDotNet, +- ADDED `BenchmarkWorker` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that is responsible for executing benchmarks within the console host. From 18608cefd26a35bdebfddbbd0c2c67c3e041451e Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Thu, 11 Dec 2025 22:24:37 +0100 Subject: [PATCH 20/32] :bookmark: add package release notes for BenchmarkDotNet extensions --- .../PackageReleaseNotes.txt | 8 ++++++++ .../PackageReleaseNotes.txt | 10 ++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt create mode 100644 .nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt new file mode 100644 index 0000000..59e6169 --- /dev/null +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt @@ -0,0 +1,8 @@ +Version: 1.0.0 +Availability: .NET 10 and .NET 9 + +# New Features +- ADDED BenchmarkContext class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that represents the command-line context for a benchmark run +- ADDED BenchmarkProgram class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that provides the main entry point for hosting and running benchmarks using BenchmarkDotNet +- ADDED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that is responsible for executing benchmarks within the console host +  \ No newline at end of file diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt new file mode 100644 index 0000000..42108fa --- /dev/null +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt @@ -0,0 +1,10 @@ +Version: 1.0.0 +Availability: .NET 10 and .NET 9 + +# New Features +- ADDED BenchmarkWorkspace class in the Codebelt.Extensions.BenchmarkDotNet namespace that provides a default implementation of IBenchmarkWorkspace for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet +- ADDED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace that specifies configuration options that is related to the BenchmarkWorkspace class +- ADDED BenchmarkWorkspaceOptionsExtensions class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the BenchmarkWorkspaceOptions class: ConfigureBenchmarkDotNet +- ADDED IBenchmarkWorkspace interface in the Codebelt.Extensions.BenchmarkDotNet namespace that defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet +- ADDED ServiceCollectionExtensions class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the IServiceCollection interface: AddBenchmarkWorkspace and AddBenchmarkWorkspace{TWorkspace} +  \ No newline at end of file From d9b5d20d73ac211f56703848ec407db393a89b0c Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Thu, 11 Dec 2025 22:24:56 +0100 Subject: [PATCH 21/32] =?UTF-8?q?=E2=9C=A8=20update=20type=20parameter=20i?= =?UTF-8?q?n=20AddBenchmarkWorkspace=20method=20for=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ServiceCollectionExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs index bcaf7c1..8030e1f 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs @@ -21,23 +21,23 @@ public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection s } /// - /// Adds a benchmark workspace implementation of type to the specified + /// Adds a benchmark workspace implementation of type to the specified /// - /// The type that implements the interface. + /// 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 , + /// 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 TImplementation : class, IBenchmarkWorkspace + 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() + .AddSingleton() .Configure(setup ?? (_ => {})) .AddSingleton(options); } From c524199fde408c888f679dcb954b8557211c528c Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Thu, 11 Dec 2025 23:05:52 +0100 Subject: [PATCH 22/32] :art: add new icon.png image to project --- .../icon.png | Bin 0 -> 5486 bytes .../Codebelt.Extensions.BenchmarkDotNet/icon.png | Bin 0 -> 5486 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .nuget/Codebelt.Extensions.BenchmarkDotNet.Console/icon.png create mode 100644 .nuget/Codebelt.Extensions.BenchmarkDotNet/icon.png diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/icon.png b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0b304b4da142478b36afec0c21a0502950bbb2e1 GIT binary patch literal 5486 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 literal 0 HcmV?d00001 diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/icon.png b/.nuget/Codebelt.Extensions.BenchmarkDotNet/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0b304b4da142478b36afec0c21a0502950bbb2e1 GIT binary patch literal 5486 zcmV-!6_M(RP)F*bPrzs=$R02JOyL_t(|ob8=?d>qA<$A7PS4&An~Eg!Ni z$up9Sbzoz0Yyt$9b0L_3*Rla~ABJ%4Lb8|-NgO9+u^0I8*&{3v4re)bZE)}f8v+{+ zhj2;3hb&`VKIKE$MwVn9Gu{364_S^Z%~aQndPW+lzn@S4G2PWw)$?Axs(N)1Gs9u{ zA_h7G4g-s$0K*(d0fsq_0t|B;1sLWy3NXxZ*%{g9>Hl~-2~h%*CxK`jz#i!fCND4E z_VlNA<{{D8RQS+L0lrHbh$#qxgT!bcqAi76pDKq<{r7$=z}KhVJK7-EkC||-AWQ)p z0I9vp8hr|2SSx@g%v_j!r!)`&I4*8xptA6R1}c1Dh2ionE7|l!V}H_*z6v0st<&%Q z2SZRT$at7wOd%ME7Mr08tXR- z80hac+;Q3P9|{BV>DF`jf=wWs$xY3TG6e3$mvmoU0+@*8uK_ zoVo`T&&+-TYU2+k)$7y0ucBqf_}o3&@6`y_`*`8IjHWR4Bn4<+;KNqbhdNBu<(5izU$ zxu76pv&1iuR-1RZl4D4z4RG*?$4f9Ygl_@1Ddye5CWy>qGEa)zZKeWfwu(o&5onXM z!ofCzAW4Z-b+S1Mke~=lCz_@dPXr2}hkfR_bmI>p2>H0X)CF?_TPy%MDiC)BOk4hS zrwT||3~BjKwYLg5079}_(80nI61B|p|7T_DvyITA;Z#0xmMM)m5K z`KDBReH%@a_n61Hv3jY0-azNcZmb8|)KFFc*@OgH192eG8NUKheEeqyfafvL{Y@ORlMtvY zfUvj*0VdHsvgd5#=KV{uBy;H!2!bv07!nGwe@RwK%~JoP%zVEzAi>v?l)QHPW7Bfu zl(pDlr2vX*uD&v8kd$%&sC^>OXBu33yv08Y-sCd@q$$13xR6|WUG-A`76O$sPyu0- zd)j$(&f*;%vCX-SsEzelqTUT0Kn19JB6qyY5DP(cE`V8Jj0BWdp@)J1lrXpiz^yO^ z0$v2Gm-)7W=?h}_8w}dNBrC;bia!JNb1*LK7JdwH zgJ}kUJDJQs)jpp0y%?oq4;th$QDo3p0j-{-zXemg>ui@v=IsDJ>Up!YbaqNWn0?*M zHb9-hgPS(JdrbWW$W5SPnH=;AKoBiF0Ip_Y(bD@~(8N{5;`NxM=kjhUEuDQ)dHLEy zYA-;p0E&%3HL;Gzfb|0Wf!YlKejmFOLUMpyc&g7`qJmf@$iQhq(0iwzbl_ zZeTxJW0ovtGrzO$3IV#58IJ+@UjY5u7bgTA0DlhfGX&h?=`>FD|G1{EcOYi-2R5hM zJAoMtXQ^-tdg@Y@Vj z5OKx6j_j@HFaAI}tEJ?OE3P8aPV2{4sG?>8ghdSy*EWDnRrK;(a!2mU+1b2!F#tCU zfg~at7uCyFs&2mm#I7Os%EGKE8exsciy8j?6A%3NU0F9?`)tE|k;E}=QO$=n%lwZr zqh)Cq;=X;MH-g`|X`B5P?W9peMciQTxUe-)`!Ant_5Hcv* z0Ghy)0A`>os_`c9U(YWrbw?VHVY*+3>~A4np(=bLqO7c}l-%6hv7Mcr5$7f5#B(e_ z1Pp=(guVK+-6Ro#angEr0>)?1zKHAFX^(6>3`Tvp=NgHy-aR4`d3kx>qN1X-$&)9a zl9Q7&&h2)OFimq1Il+4%lZOVhnMq2;Qb}~4)L^E}+%TZ-!05$_JF@Zs?&IBK!Ylyg zQAAk*z!&1ak=ow2x%bW7+}x3xrg^fnv$bF_h+r@XP1Ee!i8Xd;+y)LWV-0~Z0QOl$ zqomy$^CS{oJ%NxH;al~bpNvi%dIk?uR8zP({B|o_CoFC_sbVA|!OY3JuG=)U5=V%d zWHQk-nyeCyk^-16^J00wlWL;;3=nZhK{KM8#Y@Z~6@Wl}tcl<6AAEJYD#W!GzJLo= zqOq<5mXLt(&6Nc)$=sxpSWjNNW6~sO{9OXw6eh^b*YC=Dm|?E-967Lk?n+jgmFm(^ zu6Oy4PqL4pEQ8nUbq9lqxm_&@u@NMzrf3cpmOhN<%B3G@sQ}HJd!qkWBj#e-A z*8%YNyiZJh6riQ0#Z44*H&#sO^msq<8XZ9Z;jGw{=0ivC$N;2!H%Y62_OgO3XuM1| z>_{4al>WPIhh_1+aZhHN#@c5D`e%SH2b6^0lQaP5FykSD`0~}eCP{k;47}Cr*3LYC zO`j^j?RF<@6)<*XWh(@~D<7seesT2g3#Q8A39A52SLcolLSGGm@v^#x!7p27Heh%z z17O#XZi-;OT^7LGY5a^C&jHv4;EyKp?4F)K03aeQAsc@^V1eZpZL`rN^tG*Jd1J%H z5wZ$sEz6rOlEhoE_LA<($T;PHtTL)G+D#iREm^F4IzF{adJ0EjVPSG>YpX5!$t}42 zESHvaVm5NqH`_k?=9_Jn8#MKo5nR8zS({o7xtt13n4dRYpL@;7*LG~|{qQ6U(0qg6 z-+aCAKiF+<0hZ|=I9AUv6{K`S5~qi1Rk-H6A9#M5MQ=j=YPKCy4-0!qoDfp zu3n=FW1>5`d7LNZ+sU4!R>)hIT`=SQjMwTfQT3R-QC8#$j6QM&nK&0hoDSxh=vumM zMG!zG2o((eg2?^mbBliW5jo5FR?!&Im=COaL z2a-?x8NwAna>gN~j^2x;j7CssWjLS-^M**=Wph_n2GG?wdkX-TEFt0BpH(cts}S0A zr<($EyoW~p{`k9L$<578V&+kqnVBt(jg4XLIuW_@^Ycf>T=)P$qG-Z|GgbnSDuFE+ zu3Ooy|Am0Q&L1XpG><{+kx6Jjnh8_)0Kh~56NM{a($_O?59ooUkbj8F+lCPnYmqvp z;e=Hnpp8v&QTlIdY&q=dwY##G5b(1Qk3PNb*#^s!Kt#gl^Ns0l(7D}i-Rt$XCnY6y zR#a3N07Onsjtf8%GuyJyjivB=BC!N^SpWcNz1?5L27e4-RQJ8M!&x}m;6tz@^o)7o z3YtQ;#@y)i;>eCd9Q^uB=$)y6Kof&6vrG5@K)_!@-Dibyfa628r}S*i2ZKRZYin!z zp+kqpWM^lO%g)Xo%gmz&MEJl7OAzZeLD~zom8|h)5ICdnO;i8BW37$z!Q8ip5+^0d zc%lA_Z>nbgv~kPXMxZ6bQmzC8LcoO5;Z7!m(VYI|_QKobha5)8^}pG>7r?>Z_f4Vq zQGl$htd#8R>`?a5D^|# zCJ4qe+Y4{L-rlU!nqD`y-|+0gt-V8ez20;pN@A9bgT??A;kUIl1OQ-X_4>~SLJ4Y$ zZ@C7-c!{99l;ueV1X=7g6XGHPwExXR1zh^ zcJ6FC&0q6QTsC3M%gf6M27^-P&D3dEen%7Tg#YgDnz!$4 zZ9Np$5tdtU<#{P-W0w;ssrNHxbK}0<6;GIq*LppQs25OCy3n1`)>2M{62(`J2)F*B zNN(TjO71*NClxd*)9MOo0T{nTs6J(%ufR`zRVyA-&1|#~kt-)BXN=7B7nEFktJ|IO zOY|59W>ep=xBkPMqEUkW3cnkf5(I-CpB-q}a(1uV5h*}zQQ3pU)OQg}U-p6U^QNSc z%V(88cOu^u%9m4BRF;;+<|_cK7D6-&kCC18)mt%hbaZ$1WUc-D;`ujtJgG|&VtOjG z@!9U(8y2)2s#8AHA{w(VBPaj+XQ!oQz6zp~7HsK8VB7vZn@;T&Tv-A3m(EUU@9^vd zBkW+$aX!{{&z$`2ulHGY7P3yAUb(V$YE|(Zg0a$~4$0sEZd1u@G|W8Xs_z!hy!?r1 zg%1Ek+Sya4ocn6}7@yK=IlF}qShNZHL3^8a6R|N?DkIY0-ko{)!cf8oWX=p;Y$hce zw-6hfr3tWcqg@|VR8;0EF1^|}W7h1l(`L4?)(qJuX>WUswb4MZF9Nw`vn;YdkLdt$Mh@03MmpaPy2kqMlAy? z0DK=3!Kw%bW2OVj%o(>{Nof(=N>vWSk|JjsB3o<{jyfrC>{$#VTKOYkb1;z6gxIJmh5Jvdbm2cf!z4gsCo$XEcKtT!ODg17_m4F!0 z?@2^Pv3##!eU(ZyN(w;WKUp*erSs10qM+p3TP4C@U9)w~J-Xha13*pHs`VWmP4@wP z_p(2(1TPho%)dz@{C9WPynU~m8@K&uwC@ueIr2iupQyy6W&vDikSl<$ zbVy-;C@7w{pm#sNo8u+_{$TLvN%Q_{x2{^-+1~U+gjRy{-0sxJ5x(RvRO#pUhmK=M zqTc6#U}Q*TqIEW?#G|ADZgw?USb&(GC!=EayGwp=YN}SP52pm7h?Uv58zRFilZhuN zl;$KVwgC>nYG5H&q^6F|R5knIsHs}DKG4y0FD#TG)@I*MtmR?z@^FhrV6@+1iRN-A zAp)}>j%rDD0L0Ji+lAF2k1EOvFqqB^vNHSO2v;2>irMdpU1tU*+<_6t&HjD5-V#-Y zYN}SPpEC8LdY32l#9lm&dq4TCtt~N1emB&&43?Cuj6+S{8BHSYc5p8lGBhtLep z4Ul>e479bWcJ&-0XV5EvLu?uJyM;T1VYqA;?+^x?wu^TZAW<1kX9hX+#jZ1hY}zb3 zc8(xH+ME%iOXkEZfkZ`veA{EE4KTzRij6=LVh=N$@kwpavRf(CP&_keWBHm77<@aF z^>?U28;s5*=@xh=7^-ImZLV1N^6*pw+F*2gFR!dzskV2WO$BmLo*A^cV%^ICEH|JW z)(DFlAQ8_Da)U~y)US~T|f0UQeuf5^0`d(~zII~E{OvFZjYVvhnO z=9c@3jn!Cy(*_vg#Jdql3B9xa(1Wb}FJ85H91D=B#C14$9ONehI=z7SV^e`tie}s^ zo7*5b5bkUm*7_d&=n!LsIZAe3P>N@%02BWajCBk)=(!&&}C}olE^;8AzFk+KY>7!L>{%uajB=uZ(E#8b9eIs-x6GK z!JuDklCYGK)^eS1kr4cl^eiWVcT}Scxl?E;j^3Q-6NXs>Yqw7wfHeRtw?7e#cT!)i z+oF_#Axg_lzI>Rt+BzhS0eCj7PjZ6*&q#fxUg!5Tl3%mRg5Y?rN=1{jHktfe1Sj4Dk~P1S}^bs9uh9$$Dl_Bo{#^H$4(b z@)0S(h~+hP5VX*?3_H%HE*osqf5|TVDD8#X%1DxrSOn=W)-H$Or{rhWDqfni<`Y2w z|HrQD`HUCp{x#yHqsedjZ|Z+U8Wxg(u5fB)_|~cephf@x9lux*ElZ!Y0(cX*c342ll9d&rF!L~C8lpR41LldVdHq3DpV3^}5z%a*AfMJfK k0K*(d0fsq_0t|EhAAg;Pq8I9V5C8xG07*qoM6N<$g89f`=l}o! literal 0 HcmV?d00001 From 469effd9cdc5e93fa27bfe475fb8a368b756af81 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Fri, 12 Dec 2025 00:12:36 +0100 Subject: [PATCH 23/32] =?UTF-8?q?=E2=9C=A8=20update=20docfx=20to=20match?= =?UTF-8?q?=20benchmarkdotnet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .docfx/BuildDocfxImage.ps1 | 2 +- .docfx/PublishDocfxImage.ps1 | 4 ++-- .docfx/api/namespaces/ClassLibrary1.md | 15 --------------- ...belt.Extensions.BenchmarkDotNet.Console.md | 7 +++++++ .../Codebelt.Extensions.BenchmarkDotNet.md | 14 ++++++++++++++ .docfx/docfx.json | 16 ++++++++-------- .docfx/images/32x32.png | Bin 1619 -> 1603 bytes .docfx/images/50x50.png | Bin 2064 -> 2555 bytes .docfx/images/favicon.ico | Bin 15086 -> 15406 bytes .docfx/includes/availability-default.md | 1 - .docfx/includes/availability-modern.md | 2 +- .docfx/index.md | 10 +++++++--- .docfx/packages/index.md | 15 ++++----------- .docfx/toc.yml | 4 ++-- 14 files changed, 46 insertions(+), 44 deletions(-) delete mode 100644 .docfx/api/namespaces/ClassLibrary1.md create mode 100644 .docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md create mode 100644 .docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md delete mode 100644 .docfx/includes/availability-default.md diff --git a/.docfx/BuildDocfxImage.ps1 b/.docfx/BuildDocfxImage.ps1 index b0257f4..63fd259 100644 --- a/.docfx/BuildDocfxImage.ps1 +++ b/.docfx/BuildDocfxImage.ps1 @@ -1,4 +1,4 @@ $version = minver -i -t v -v w docfx metadata docfx.json -docker buildx build -t yourbranding/classlibrary1:$version --platform linux/arm64,linux/amd64 --load -f Dockerfile.docfx . +docker buildx build -t benchmarkdotnet-docfx:$version --platform linux/arm64,linux/amd64 --load -f Dockerfile.docfx . get-childItem -recurse -path api -include *.yml, .manifest | remove-item diff --git a/.docfx/PublishDocfxImage.ps1 b/.docfx/PublishDocfxImage.ps1 index b973de7..267de57 100644 --- a/.docfx/PublishDocfxImage.ps1 +++ b/.docfx/PublishDocfxImage.ps1 @@ -1,3 +1,3 @@ $version = minver -i -t v -v w -docker tag classlibrary1-docfx:$version yourbranding/classlibrary1:$version -docker push yourbranding/classlibrary1:$version +docker tag benchmarkdotnet-docfx:$version jcr.codebelt.net/geekle/benchmarkdotnet-docfx:$version +docker push jcr.codebelt.net/geekle/benchmarkdotnet-docfx:$version diff --git a/.docfx/api/namespaces/ClassLibrary1.md b/.docfx/api/namespaces/ClassLibrary1.md deleted file mode 100644 index e4d0124..0000000 --- a/.docfx/api/namespaces/ClassLibrary1.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -uid: ClassLibrary1 -summary: *content ---- -The `ClassLibrary1` namespace contains types that ... - -[!INCLUDE [availability-default](../../includes/availability-default.md)] - -Complements: [xUnit: Capturing Output](https://xunit.net/docs/capturing-output) 🔗 - -### Extension Methods - -|Type|Ext|Methods| -|--:|:-:|---| -|ClassLibrary1|⬇️|`Awesome`| diff --git a/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md new file mode 100644 index 0000000..73b7215 --- /dev/null +++ b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md @@ -0,0 +1,7 @@ +--- +uid: Codebelt.Extensions.BenchmarkDotNet.Console +summary: *content +--- +The `Codebelt.Extensions.BenchmarkDotNet.Console` namespace contains types that provide a structured and opinionated console-hosted execution model for `BenchmarkDotNet`. + +[!INCLUDE [availability-modern](../../includes/availability-modern.md)] diff --git a/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md new file mode 100644 index 0000000..a1e8fc1 --- /dev/null +++ b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md @@ -0,0 +1,14 @@ +--- +uid: Codebelt.Extensions.BenchmarkDotNet +summary: *content +--- +The `Codebelt.Extensions.BenchmarkDotNet` namespace contains types that provide a uniform, opinionated, and extensible way of working with `BenchmarkDotNet`. + +[!INCLUDE [availability-modern](../../includes/availability-modern.md)] + +### Extension Methods + +|Type|Ext|Methods| +|--:|:-:|---| +|BenchmarkWorkspaceOptions|⬇️|`ConfigureBenchmarkDotNet`| +|IServiceCollection|⬇️|`AddBenchmarkWorkspace`, `AddBenchmarkWorkspace`| diff --git a/.docfx/docfx.json b/.docfx/docfx.json index 102877c..1988f6b 100644 --- a/.docfx/docfx.json +++ b/.docfx/docfx.json @@ -4,7 +4,8 @@ "src": [ { "files": [ - "ClassLibrary1/**.csproj" + "Codebelt.Extensions.BenchmarkDotNet/**.csproj", + "Codebelt.Extensions.BenchmarkDotNet.Console/**.csproj" ], "src": "../src" } @@ -12,15 +13,14 @@ "dest": "api", "filter": "filterConfig.yml", "properties": { - "TargetFramework": "net8.0" + "TargetFramework": "net10.0" } } ], "build": { "xref": [ "https://docs.cuemon.net/xrefmap.yml", - "https://docs.savvyio.net/xrefmap.yml", - "https://sharedkernel.codebelt.net/xrefmap.yml", + "https://bootstrapper.codebelt.net/xrefmap.yml", "https://github.com/dotnet/docfx/raw/main/.xrefmap.json" ], "content": [ @@ -46,15 +46,15 @@ } ], "globalMetadata": { - "_appTitle": "Shared Kernel (DDD) for .NET", - "_appFooter": "Generated by DocFX. Copyright 2024 ClassLibrary1. All rights reserved.", + "_appTitle": "Extensions for BenchmarkDotNet by Codebelt", + "_appFooter": "Generated by DocFX. Copyright 2025 Geekle. All rights reserved.", "_appLogoPath": "images/50x50.png", "_appFaviconPath": "images/favicon.ico", - "_googleAnalyticsTagId": "G-X000000000", + "_googleAnalyticsTagId": "G-K2NG2TXDWQ", "_enableSearch": false, "_disableContribution": false, "_gitContribute": { - "repo": "https://github.com/classlibrary1/ClassLibrary1", + "repo": "https://github.com/codebeltnet/benchmarkdotnet", "branch": "main" }, "_gitUrlPattern": "github" diff --git a/.docfx/images/32x32.png b/.docfx/images/32x32.png index 9d3ae92d0e1ab063d50d2ac17d82ffe66f4f4fde..a68f4e44c694708c61740896c79d2109da0ef868 100644 GIT binary patch delta 1597 zcmV-D2EzH%48shN8Gi-<0047(dh`GQ010qNS#tmY2MquK2Mqxuy;D2@000?uMObuG zZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs0000HbVXQnRB3cll6;s5{zvq?ljR9J<@mu+lY)fvZs&$-w3P3$C;vbIq|LVuc=G>u6n+I4`6M8_&v zTPl@S)rJZZ+ZasUgoMyaHEFPEQr8cg#zv>I2B~5KB%noVHK|`%lZh`wT`Pygb%K*F zZPKt8v^cgCzuspb^5(>$r8OG=Psh*Ed47LC_n!0IBdW@mDSNqKxb?X@lm)#6Yy|$vbT0z6_ z`VqRC`c6JoE_hDC{^NV+#>4H89u;^ROLhRiUu3{z5`SxEMiTj=;7jEt(XS$)teJ}~ z7$6CNa{XN~e7LzG>v~h`4qp89MhtjJq6mu2$Lxi{Bp~oP4CImHhHM1*(Tx}|B#~K4 zi2*})7JnTB?}&vqZ$Kn)BX9r@nc3_M413Y&8U=_RYFjBLzge_-7f=ms96S^}=ny6En5{uRR#gx zeCGC`;_ef;2e|W%AJ&Z(EP3sTSM*cW<`0dSh=|yYjg3{dy=+SyHlJ$ z5V+0L+VR-ImbOQ?Eyu4}xuQpW8pF|!MD69b$G2~KSo%ZHsO_DmrhX{ltQg-?b3FF< zxdq)n7yIgX-2iC+V@Jn!iip+H(sHZox_?>Mbwzcdl!3r)o`&GIVhQ4Eh=y8&^T4ue!Q<)n~vvCle#v^ZBV?IKG;f>(}1DV`&xs^|yOB#fO@!n15D0 z)r&hLERvMQb*9XVG$SAo&c-dl587yxfo(o5yy>I+7m!CfD_-c+A zu{+~S;B&eh>p4;U+bhY{9+Ft3`sQNu`^O^T0<%X!sSM_pWPD`z$k1Od0e=jH-~2;! z+atZ%O#Jed;lEy9Zp~6znh+5|RNWE-hV(U?(be(rOB4U>y;K|2r&!TPKkuT{wSq{7-Wv(I#(#6T*LelL)727vn%x8LBB z{{n7Cd9QoM;I=E)Oh6b+*_o5ov-9(ZPVK8>QxY)ui=#)x(Xco=EH)Kd*1rc3M@Plc v;n|!=#BVm&zB)EFKmTRlj+Z@l|DXCFV1`se5L8#600000NkvXXu0mjfWeOd2 literal 1619 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7e6l0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#N0|QfNfKP}kgG(KQXCqJb*E4uDGI%yIxHmBPG&7_vVen~T z@M&Z4Yh`e*W^k)xaIIo+u40Iu$lzAb;0=^&V(@BYaII$W>tqO+#o$)M;8M%r*1!-3 zrKF%Q^V<&x0=eu-R`888La@()Vj*TjdgU$?b=a;ys=w=9jJ+k4#1q&ay1+Uxl zv(msE#pdba7-Dg{_R{m{%K;*6ALh(S>R2p#`ATOO&(YSPR~#WM-P^jjGF%;4UpwgL z-QgDR>Jk?3l0K;WZtwa3sc-hqP~(65`|Z8w(|^yI{hRAhOK0R{h9ePzYU#U+ZGs;) ztaLHSwK~Q&i*L&v3k84ndy?BuC@q{m<%IJ0cbBI({+W4`we|0tMI1&87S{`%XJ8ea zvx=`^+WHeqZ?WHENPm9rBYVZObK2+T^Eb0~H*qtxc{mG-i|CgGhj@9N7Sr6MwIPr} zY&z@7S*lNG9B9zd{rdWTNJx~+Ril&y3B7p7tESqg|0-^d*gA_bV)L}D>o1$v-xn}z z+||qTV42LVJ+4z3x8)hjZ=Bm))mx^rI!Woxom95nmN$f#?fuQre*2reL36W=3|H{s zhsrNL$ZoxJIGbUWP1&8x65AS9X$Z)^J#w;Q!OM`pFOAQpCpQ&8cUO4S+C4p~;g#5w z#3kHQPM6v?YinN8sMR-p`)aG4vyuF^KzDWK(}WOdab+S_SSt4d!I3#|KId)cYmv8r#H*nEjrYwXB5qJFCgG` z-|x%MuPLRU`jMy5)Dpk1?)Sgu=g-&Qua|ki@=<@~|I+K6&t=PjiA1%;HKHUXu_VM=BiSi&nI;RMvf;OXk;vd$@?2>@#0FSY;x diff --git a/.docfx/images/50x50.png b/.docfx/images/50x50.png index bc05e4137abcaf8f5b9b69e408eef5f89a2edb48..6c4253ce9499c76914c4704358a3bc12720299d5 100644 GIT binary patch delta 2545 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 Z8Y2|a;2+~xT*^5x02bqDU3=O+=5P1#tEB({ diff --git a/.docfx/images/favicon.ico b/.docfx/images/favicon.ico index b3065baffc747dbd38169d8f88289268de20f89b..18debfabd26008168beb1214512e52bcf1fddf61 100644 GIT binary patch literal 15406 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 LZV&ut_rU)H*?c$u literal 15086 zcmd6t35;D+7{||;wo_ZCAr)FNqp3#hJJHsNXbGbBJydM9uZdl!8WkkgYDAGBN{B7Q zQY39kW3O0Bl$zQaRE@0$^`77V&O7g&d+ze?ySXQk-->;Bp8T;J10Q+*T;Eyiz4*8s z+SBr&zXR?E|5_-AF=@7bANsm)ZiTElCh2KBo(rvhjJP&>?QkFX{%%2*`94B-Yxp|G zkMFl$`yPC>#pRkLLw{3v!;AeT?H!YBd^x%odiM45jO+gnA3OWLv$U~05j1A3UyX}t z8Ey5v)U(0b<}IJaM?>e{=7-$~SnTESn(a?3vDp9?dA50u;fAz0X??`jv*|G}hZSiy zwhtTKe@i{vJo#U0edZ~NKg-KubrwI^bb#I|d2)7buFVoJNr(86=TMTaD=#qrIrz%- z+;rosj-BR-^T9nIWnV9@jZIPK|5N#ji&0O8-EOb~{?GjXA^!RGa`pSM(b%2o=dQM| z8`nm!sPnILw*)f3ca#tMhr&+~I`?YF8rO@G5NU$MM_fXpJ!uw3dnFE}3r= z*G8}C{FTeca4HOk(7Hrz*aR+tZy`2!$>Z|%W#}~{f90h4fZmI@gXYU8fadqQ*FS*O zkeIvr{9NPO=rt>UKR3;3d|7HcE?-}kA%DfyoL<-Z7ic&SY*O<~>i2VvYh&91N$+1h z(=P=*pSOUqa3g50nJcG8?BeqEW%x_Ve<6L_#pr9TqdlBu(9~n_W5>10AufNN-*IvJ zR!09Rp!Ipvj={!CD~Z1ueBtHsFzwX64Ei^OH$1x{kEN8Bx9ubTM&Pg8ueAMXC3g2V zG5=rEV%t9AYfs|^FORJG&-Uz!Jpb?89P5?jFx|@|%l*@ijphnP=Ir{qvtHh|pB%=4 zJBM0Mdso{ZSJHn3{0?r+qUzemnh+N^PKK`TA%Fcj7ul!;eZ3Dpgrf5G{XB~Ab>dx#f;IY{fxVt9H^U!LbdE{>6ifTBd?{P= zAAU5Rwf@ljEh%PEa;-T}gSFQ@^RNeej)Chy@8>=!Drfmx3^O3pUO9RipP{u}INSJE z#ZK!z%{eu;G(I$MxGx8?l@IwE0d@01NAoPr6>U7#H;m7AU`feS&+I3`eV(ZP6pR>H z*+#P=$Gf2farf=@QHy~kA@9hZoO3LTs1NA-i2VL`^7XI5XY`F6SZ6$(4f$yw$3>1o`=oZmcJo8VPIt&&4wJ?;B3%4PVWq%`Z+KpYBBI6NRZd`<^^_bK??Km-yO7Dl!|0~R7#4V-Ctg&f_DEFM0vinY1?u^rENC0%fw8t$sUC5QxRF!=kxG~Es=WSZ0q=TOlh%c?)f=Yi- zps)Ya>V&*CnJ2+8wbwi_@&9D# zYX!~0tbV;x`z8m}qDE`R(z#Giwbmwj&|*n>ga6+cCY836usloYh;{f`u>a0 z@lYL41;&2@zeAJ!SvfjWp?dwS+%M5-^!Rm65A*%FZTfeF&!HLdy+UnH$rsI(c$P{7!_=E#aGrZ^ei_Taf7*=-lLq zEt`emBdWFN)3IqUUwS`6adV!~2l>JflA;w(gT>VNVFfRvUD`rLJx19|^*z4J%cZEG~Z&Lry_O^)Sk6+#6lalU*qUFTWdc_{Q%2hD2 z=r~FKh;;?@f@14BJOYX9k|aNQ<;2q1(i~+bTm&2E^_9ntZHKAl9`;t|vHj59I=yV# zmz7M%&(K#gT@Gq@^p;FJI~=XudU2K*O{Kk{D@03qGtpIu=y4(9t z^Nq!z=Yak%Uhh4<14Hi#+Fg}CYiG-skO%to}PSY zk7cEmQ`fWSL9lkV)Hvo!gn!6xldhFgm%Zl4*3K=bso&8ZN&7A7IG#3c2 z52cGqb>~4ew pkZIoI`(4c*Y9HD3jx(l*t Date: Fri, 12 Dec 2025 00:13:08 +0100 Subject: [PATCH 24/32] :package: updated NuGet package definition --- .../README.md | 46 +++++++++++++++++++ .../README.md | 37 +++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 .nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md create mode 100644 .nuget/Codebelt.Extensions.BenchmarkDotNet/README.md diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md new file mode 100644 index 0000000..5cc9063 --- /dev/null +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md @@ -0,0 +1,46 @@ +# Codebelt.Extensions.BenchmarkDotNet.Console + +A structured, host-based execution model for running BenchmarkDotNet benchmarks in console applications. + +## About + +**Codebelt.Extensions.BenchmarkDotNet.Console** extends the **Codebelt.Extensions.BenchmarkDotNet** package with a dedicated console runner built on the Microsoft Generic Host. + +It provides a predictable startup model, consistent dependency injection, and a managed application lifecycle for benchmark execution, aligning benchmarks with the same hosting principles used in modern .NET applications. + +By embracing `Microsoft.Extensions.Hosting`, this package enables clean separation between benchmark definition, configuration, and execution - making benchmarks easier to compose, test, and evolve over time. + +At its core, the package favors convention over configuration and promotes benchmarks as first-class, host-managed workloads rather than ad-hoc console routines. + +## CSharp Example + +Benchmarks are executed through a console-hosted Generic Host, allowing full participation in dependency injection, configuration, logging and more. + +```csharp +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using Codebelt.Extensions.BenchmarkDotNet.Console; + +public class Program +{ + public static void Main(string[] args) + { + BenchmarkProgram.Run(args, o => + { + o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; + o.ConfigureBenchmarkDotNet(c => + { + var slimJob = BenchmarkWorkspaceOptions.Slim; + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) + .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + }); + }); + } +} +``` + +## Related Packages + +* [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) 📦 +* [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) 📦 diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md b/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md new file mode 100644 index 0000000..13a9620 --- /dev/null +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md @@ -0,0 +1,37 @@ +# Codebelt.Extensions.BenchmarkDotNet + +A unified, opinionated foundation for building robust BenchmarkDotNet workflows in .NET. + +## About + +**Codebelt.Extensions.BenchmarkDotNet** is part of a modern, MIT-licensed ecosystem designed to bring clarity, structure, and consistency to BenchmarkDotNet projects. + +If you value predictable conventions, clean separation of responsibilities, and benchmarks that scale gracefully across `.NET 9` and `.NET 10`, this library is your agile companion. + +It removes unnecessary ceremony while embracing best practices from other consumers of BenchmarkDotNet, so you can focus on performance insights, not plumbing. + +At its heart, the package is **free, flexible, and crafted to extend and empower your agile codebelt**. + +## CSharp Example + +Benchmarks are executed using a Generic Host–based bootstrap model, allowing BenchmarkDotNet to participate in a fully managed application lifecycle with dependency injection, configuration, and logging. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; + +var hostBuilder = Host.CreateDefaultBuilder(args); +hostBuilder.ConfigureServices(services => +{ + services.AddSingleton(new BenchmarkContext(args)); + services.AddBenchmarkWorkspace(setup); +}); +var host = hostBuilder.Build(); +host.Run(); +``` + +## Related Packages + +* [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) 📦 +* [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) 📦 From e69f60cc0ba695d63f8d4772713e2c649142ef49 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Fri, 12 Dec 2025 00:13:34 +0100 Subject: [PATCH 25/32] :art: add property group with description and tags to csproj files --- .../Codebelt.Extensions.BenchmarkDotNet.Console.csproj | 5 +++++ .../Codebelt.Extensions.BenchmarkDotNet.csproj | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj b/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj index ae58d14..7e8e79f 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj @@ -1,5 +1,10 @@  + + The Codebelt.Extensions.BenchmarkDotNet.Console namespace contains types that provide a structured and opinionated console-hosted execution model for BenchmarkDotNet. + benchmark benchmarkdotnet console hosting generic-host dependency-injection performance + + diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj b/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj index 9e03c77..87bb561 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj +++ b/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj @@ -1,5 +1,10 @@  - + + + The Codebelt.Extensions.BenchmarkDotNet namespace contains types that provide a uniform, opinionated, and extensible way of working with BenchmarkDotNet. + benchmark benchmarkdotnet performance microbenchmark profiling diagnostics + + From 4254e60356e140c6e551e274fe0d703fdc5fecf2 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Fri, 12 Dec 2025 00:13:57 +0100 Subject: [PATCH 26/32] :art: update exception documentation for ConfigureBenchmarkDotNet method --- .../BenchmarkWorkspaceOptionsExtensions.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs index 0f9c12a..a16659a 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs @@ -55,6 +55,13 @@ public static class BenchmarkWorkspaceOptionsExtensions /// 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); From 9300d7df22e8a694b1c4ec15f8a9c4b71bbec1ca Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Fri, 12 Dec 2025 00:14:23 +0100 Subject: [PATCH 27/32] :art: update readme to reflect benchmarkdotnet extensions --- README.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b5cf6c1..478e789 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,29 @@ -![ClassLibrary1](.nuget/ClassLibrary1/icon.png) +![Extensions for BenchmarkDotNet API by Codebelt](.nuget/Codebelt.Extensions.BenchmarkDotNet/icon.png) -# Classlibrary1 API by Codebelt +# Extensions for BenchmarkDotNet API by Codebelt -[![ClassLibrary1 CI/CD Pipeline](https://github.com/codebeltnet/ClassLibrary1/actions/workflows/pipelines.yml/badge.svg)](https://github.com/codebeltnet/ClassLibrary1/actions/workflows/pipelines.yml) [![codecov](https://codecov.io/gh/codebeltnet/ClassLibrary1/graph/badge.svg?token=WAmfmpQyCz)](https://codecov.io/gh/codebeltnet/ClassLibrary1) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ClassLibrary1&metric=alert_status)](https://sonarcloud.io/dashboard?id=ClassLibrary1) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ClassLibrary1&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=ClassLibrary1) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=ClassLibrary1&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=ClassLibrary1) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ClassLibrary1&metric=security_rating)](https://sonarcloud.io/dashboard?id=ClassLibrary1) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/codebeltnet/ClassLibrary1/badge)](https://scorecard.dev/viewer/?uri=github.com/codebeltnet/ClassLibrary1) +[![BenchmarkDotNet CI/CD Pipeline](https://github.com/codebeltnet/benchmarkdotnet/actions/workflows/ci-pipeline.yml/badge.svg)](https://github.com/codebeltnet/benchmarkdotnet/actions/workflows/ci-pipeline.yml)[![codecov](https://codecov.io/gh/codebeltnet/benchmarkdotnet/graph/badge.svg?token=qX3lHGvFS4)](https://codecov.io/gh/codebeltnet/benchmarkdotnet) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=benchmarkdotnet&metric=alert_status)](https://sonarcloud.io/dashboard?id=benchmarkdotnet) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=benchmarkdotnet&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=benchmarkdotnet) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=benchmarkdotnet&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=benchmarkdotnet) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=benchmarkdotnet&metric=security_rating)](https://sonarcloud.io/dashboard?id=benchmarkdotnet) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/codebeltnet/benchmarkdotnet/badge)](https://scorecard.dev/viewer/?uri=github.com/codebeltnet/benchmarkdotnet) -Provides a focused API for .NET class library projects following [Microsoft Engineering Guidelines](https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines) as well as Conventions, Idioms and Patterns by [Codebelt](https://github.com/codebeltnet#conventions-idioms-and-patterns). +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. -Full documentation (generated by [DocFx](https://github.com/dotnet/docfx)) located here: https://xxx.yyy.zzz/ +Your versatile BenchmarkDotNet companion for modern development with `.NET 9` and `.NET 10`. -## 📦 Standalone Packages +It is, by heart, free, flexible and built to extend and boost your agile codebelt. -Provides a focused API for ... +## 📚 Documentation -|Package|vNext|Stable|Downloads| -|:--|:-:|:-:|:-:| -| [ClassLibrary1](https://www.nuget.org/packages/ClassLibrary1/) | ![vNext](https://img.shields.io/nuget/vpre/ClassLibrary1?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/ClassLibrary1?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/ClassLibrary1?color=blueviolet&logo=nuget) | +Full documentation (generated by [DocFx](https://github.com/dotnet/docfx)) located here: https://benchmarkdotnet.codebelt.net/ -## 🏭 Productivity Packages +## 📦 Standalone Packages -Provides a convenient set of default API additions for ... +Provides a focused API for BenchmarkDotNet projects. |Package|vNext|Stable|Downloads| |:--|:-:|:-:|:-:| -| [ClassLibrary1.App](https://www.nuget.org/packages/ClassLibrary1.App/) | ![vNext](https://img.shields.io/nuget/vpre/ClassLibrary1.App?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/ClassLibrary1.App?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/ClassLibrary1.App?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.BenchmarkDotNet?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.BenchmarkDotNet?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.BenchmarkDotNet?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.BenchmarkDotNet.Console?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.BenchmarkDotNet.Console?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.BenchmarkDotNet.Console?color=blueviolet&logo=nuget) | -### Contributing to `Extensions for ClassLibrary1 API by Codebelt` +### Contributing to `Extensions for BenchmarkDotNet API by Codebelt` [Contributions](.github/CONTRIBUTING.md) are welcome and appreciated. Feel free to submit issues, feature requests, or pull requests to help improve this library. From 91a2c47d68be23cc79d5770048703f5e6f7d288e Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Fri, 12 Dec 2025 00:32:33 +0100 Subject: [PATCH 28/32] :chart_with_upwards_trend: add benchmark reports --- ...nchmarkWorkspaceBenchmark-report-github.md | 20 +++---- ...WorkspaceOptionsBenchmark-report-github.md | 44 +++++++-------- ...BenchmarkContextBenchmark-report-github.md | 54 +++++++++++++++++++ ...BenchmarkProgramBenchmark-report-github.md | 28 ++++++++++ ....BenchmarkWorkerBenchmark-report-github.md | 26 +++++++++ 5 files changed, 140 insertions(+), 32 deletions(-) create mode 100644 reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkContextBenchmark-report-github.md create mode 100644 reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkProgramBenchmark-report-github.md create mode 100644 reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkWorkerBenchmark-report-github.md diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md index 2078082..6747bf6 100644 --- a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md @@ -1,10 +1,10 @@ ``` -BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7309) +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) 12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores -.NET SDK 10.0.100 - [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 - Job-LDLMHG : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 @@ -13,10 +13,10 @@ MinIterationCount=15 WarmupCount=1 ``` | Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | |-------------------------------------------------------------- |---------- |-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-------:|--------:|-------:|----------:|------------:| -| 'Construct BenchmarkDotNetWorkspace' | .NET 10.0 | 2.163 μs | 0.0318 μs | 0.0282 μs | 2.166 μs | 2.125 μs | 2.206 μs | 1.00 | 0.02 | 0.2659 | 4248 B | 1.00 | -| 'Load assemblies from tuning folder (no matching assemblies)' | .NET 10.0 | 406.714 μs | 17.1587 μs | 19.7600 μs | 409.632 μs | 386.064 μs | 455.586 μs | 188.03 | 9.23 | - | 15913 B | 3.75 | -| 'PostProcessArtifacts (move results -> tuning folder)' | .NET 10.0 | 10.829 μs | 0.2468 μs | 0.2743 μs | 10.775 μs | 10.421 μs | 11.538 μs | 5.01 | 0.14 | - | 392 B | 0.09 | +| 'Construct BenchmarkDotNetWorkspace' | .NET 10.0 | 2.162 μs | 0.0289 μs | 0.0241 μs | 2.162 μs | 2.131 μs | 2.213 μs | 1.00 | 0.02 | 0.2678 | 4248 B | 1.00 | +| 'Load assemblies from tuning folder (no matching assemblies)' | .NET 10.0 | 434.051 μs | 32.0943 μs | 36.9599 μs | 422.094 μs | 395.167 μs | 523.232 μs | 200.80 | 16.84 | - | 15913 B | 3.75 | +| 'PostProcessArtifacts (move results -> tuning folder)' | .NET 10.0 | 10.441 μs | 0.2018 μs | 0.2073 μs | 10.398 μs | 10.196 μs | 10.922 μs | 4.83 | 0.11 | - | 392 B | 0.09 | | | | | | | | | | | | | | | -| 'Construct BenchmarkDotNetWorkspace' | .NET 9.0 | 3.041 μs | 0.0650 μs | 0.0749 μs | 3.053 μs | 2.937 μs | 3.181 μs | 1.00 | 0.03 | 0.2656 | 4272 B | 1.00 | -| 'Load assemblies from tuning folder (no matching assemblies)' | .NET 9.0 | 400.892 μs | 15.9646 μs | 18.3849 μs | 398.322 μs | 380.181 μs | 441.577 μs | 131.89 | 6.70 | - | 15817 B | 3.70 | -| 'PostProcessArtifacts (move results -> tuning folder)' | .NET 9.0 | 10.772 μs | 0.2729 μs | 0.3143 μs | 10.744 μs | 10.320 μs | 11.353 μs | 3.54 | 0.13 | - | 392 B | 0.09 | +| 'Construct BenchmarkDotNetWorkspace' | .NET 9.0 | 3.030 μs | 0.0734 μs | 0.0815 μs | 3.055 μs | 2.885 μs | 3.190 μs | 1.00 | 0.04 | 0.2628 | 4272 B | 1.00 | +| 'Load assemblies from tuning folder (no matching assemblies)' | .NET 9.0 | 422.517 μs | 13.7315 μs | 15.8133 μs | 416.600 μs | 405.125 μs | 460.289 μs | 139.56 | 6.30 | - | 15817 B | 3.70 | +| 'PostProcessArtifacts (move results -> tuning folder)' | .NET 9.0 | 10.349 μs | 0.2326 μs | 0.2585 μs | 10.293 μs | 9.973 μs | 10.872 μs | 3.42 | 0.12 | - | 392 B | 0.09 | diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md index fa89bd8..bea3e61 100644 --- a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md @@ -1,10 +1,10 @@ ``` -BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7309) +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) 12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores -.NET SDK 10.0.100 - [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 - Job-LDLMHG : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 @@ -13,32 +13,32 @@ MinIterationCount=15 WarmupCount=1 ``` | Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | |------------------------------------------------- |---------- |--------------:|-----------:|-----------:|--------------:|--------------:|--------------:|------:|--------:|-------:|----------:|------------:| -| 'PostConfigureOptions - default config' | .NET 10.0 | 2,110.8640 ns | 56.8229 ns | 65.4374 ns | 2,095.9841 ns | 2,013.1433 ns | 2,238.9894 ns | ? | ? | 0.2705 | 4248 B | ? | -| 'PostConfigureOptions - custom config' | .NET 10.0 | 2,095.8151 ns | 68.6064 ns | 76.2558 ns | 2,101.2500 ns | 1,977.0449 ns | 2,221.1213 ns | ? | ? | 0.3073 | 4840 B | ? | +| 'PostConfigureOptions - default config' | .NET 10.0 | 2,078.7319 ns | 58.4506 ns | 62.5414 ns | 2,071.1990 ns | 1,973.5708 ns | 2,178.6508 ns | ? | ? | 0.2657 | 4248 B | ? | +| 'PostConfigureOptions - custom config' | .NET 10.0 | 2,063.4760 ns | 62.8084 ns | 69.8114 ns | 2,058.5810 ns | 1,975.1579 ns | 2,212.9844 ns | ? | ? | 0.3027 | 4840 B | ? | | | | | | | | | | | | | | | -| 'PostConfigureOptions - default config' | .NET 9.0 | 2,986.9093 ns | 55.9419 ns | 52.3281 ns | 3,005.3757 ns | 2,905.3320 ns | 3,077.9813 ns | ? | ? | 0.2603 | 4248 B | ? | -| 'PostConfigureOptions - custom config' | .NET 9.0 | 3,044.9700 ns | 24.8852 ns | 20.7803 ns | 3,042.8357 ns | 2,998.1666 ns | 3,071.4844 ns | ? | ? | 0.3000 | 4840 B | ? | +| 'PostConfigureOptions - default config' | .NET 9.0 | 2,933.8417 ns | 57.4521 ns | 53.7407 ns | 2,921.3425 ns | 2,860.3389 ns | 3,030.9748 ns | ? | ? | 0.2644 | 4248 B | ? | +| 'PostConfigureOptions - custom config' | .NET 9.0 | 3,027.1892 ns | 69.3406 ns | 77.0719 ns | 3,014.0595 ns | 2,911.8273 ns | 3,165.0277 ns | ? | ? | 0.3041 | 4840 B | ? | | | | | | | | | | | | | | | -| 'Create default BenchmarkWorkspaceOptions' | .NET 10.0 | 2,072.5817 ns | 46.8350 ns | 52.0570 ns | 2,064.2872 ns | 2,003.2817 ns | 2,172.9578 ns | 1.00 | 0.03 | 0.2550 | 4120 B | 1.00 | -| 'Create and configure BenchmarkWorkspaceOptions' | .NET 10.0 | 2,030.7542 ns | 68.5204 ns | 76.1602 ns | 2,004.4376 ns | 1,941.1354 ns | 2,174.2652 ns | 0.98 | 0.04 | 0.2548 | 4120 B | 1.00 | +| 'Create default BenchmarkWorkspaceOptions' | .NET 10.0 | 2,112.7347 ns | 40.1347 ns | 42.9437 ns | 2,107.3630 ns | 2,023.5091 ns | 2,166.5292 ns | 1.00 | 0.03 | 0.2575 | 4120 B | 1.00 | +| 'Create and configure BenchmarkWorkspaceOptions' | .NET 10.0 | 2,060.7947 ns | 40.9970 ns | 43.8663 ns | 2,051.6384 ns | 1,997.5080 ns | 2,135.4734 ns | 0.98 | 0.03 | 0.2585 | 4120 B | 1.00 | | | | | | | | | | | | | | | -| 'Create default BenchmarkWorkspaceOptions' | .NET 9.0 | 2,979.9549 ns | 59.5392 ns | 63.7062 ns | 2,978.6564 ns | 2,892.7934 ns | 3,115.2483 ns | 1.00 | 0.03 | 0.2570 | 4120 B | 1.00 | -| 'Create and configure BenchmarkWorkspaceOptions' | .NET 9.0 | 2,985.9997 ns | 74.5567 ns | 82.8695 ns | 3,015.2933 ns | 2,867.5636 ns | 3,138.8891 ns | 1.00 | 0.03 | 0.2534 | 4120 B | 1.00 | +| 'Create default BenchmarkWorkspaceOptions' | .NET 9.0 | 3,008.0985 ns | 41.5610 ns | 36.8428 ns | 3,006.9752 ns | 2,957.3965 ns | 3,084.6125 ns | 1.00 | 0.02 | 0.2564 | 4120 B | 1.00 | +| 'Create and configure BenchmarkWorkspaceOptions' | .NET 9.0 | 3,018.5043 ns | 64.9885 ns | 72.2346 ns | 3,020.6452 ns | 2,898.3530 ns | 3,166.4212 ns | 1.00 | 0.03 | 0.2543 | 4120 B | 1.00 | | | | | | | | | | | | | | | -| 'Full lifecycle - create, configure, validate' | .NET 10.0 | 2,174.6726 ns | 54.0249 ns | 57.8061 ns | 2,180.9386 ns | 2,077.8922 ns | 2,284.7667 ns | ? | ? | 0.2779 | 4464 B | ? | +| 'Full lifecycle - create, configure, validate' | .NET 10.0 | 2,133.8326 ns | 41.8069 ns | 39.1062 ns | 2,127.8066 ns | 2,091.4368 ns | 2,212.8536 ns | ? | ? | 0.2765 | 4464 B | ? | | | | | | | | | | | | | | | -| 'Full lifecycle - create, configure, validate' | .NET 9.0 | 3,043.1674 ns | 63.6125 ns | 70.7051 ns | 3,018.4952 ns | 2,960.1038 ns | 3,190.1593 ns | ? | ? | 0.2764 | 4464 B | ? | +| 'Full lifecycle - create, configure, validate' | .NET 9.0 | 3,059.8789 ns | 67.8953 ns | 78.1884 ns | 3,053.1393 ns | 2,965.1217 ns | 3,244.5957 ns | ? | ? | 0.2740 | 4464 B | ? | | | | | | | | | | | | | | | -| 'Property access - RepositoryPath' | .NET 10.0 | 0.6960 ns | 0.0311 ns | 0.0276 ns | 0.6961 ns | 0.6520 ns | 0.7445 ns | ? | ? | - | - | ? | -| 'Property access - Configuration' | .NET 10.0 | 0.6842 ns | 0.0374 ns | 0.0312 ns | 0.6787 ns | 0.6287 ns | 0.7299 ns | ? | ? | - | - | ? | +| 'Property access - RepositoryPath' | .NET 10.0 | 0.6729 ns | 0.0517 ns | 0.0484 ns | 0.6575 ns | 0.6005 ns | 0.7820 ns | ? | ? | - | - | ? | +| 'Property access - Configuration' | .NET 10.0 | 0.6834 ns | 0.0442 ns | 0.0414 ns | 0.6914 ns | 0.6219 ns | 0.7441 ns | ? | ? | - | - | ? | | | | | | | | | | | | | | | -| 'Property access - RepositoryPath' | .NET 9.0 | 0.7740 ns | 0.0509 ns | 0.0476 ns | 0.7728 ns | 0.6898 ns | 0.8455 ns | ? | ? | - | - | ? | -| 'Property access - Configuration' | .NET 9.0 | 0.7484 ns | 0.0522 ns | 0.0489 ns | 0.7393 ns | 0.6860 ns | 0.8567 ns | ? | ? | - | - | ? | +| 'Property access - RepositoryPath' | .NET 9.0 | 0.7156 ns | 0.0646 ns | 0.0604 ns | 0.6920 ns | 0.6508 ns | 0.8309 ns | ? | ? | - | - | ? | +| 'Property access - Configuration' | .NET 9.0 | 0.7295 ns | 0.0387 ns | 0.0362 ns | 0.7216 ns | 0.6821 ns | 0.8011 ns | ? | ? | - | - | ? | | | | | | | | | | | | | | | -| 'Property modification - set all properties' | .NET 10.0 | 2,059.4134 ns | 54.0381 ns | 62.2303 ns | 2,050.0560 ns | 1,950.9536 ns | 2,162.7347 ns | ? | ? | 0.2571 | 4120 B | ? | +| 'Property modification - set all properties' | .NET 10.0 | 2,145.0926 ns | 39.4528 ns | 34.9739 ns | 2,140.4873 ns | 2,100.3063 ns | 2,195.2403 ns | ? | ? | 0.2594 | 4120 B | ? | | | | | | | | | | | | | | | -| 'Property modification - set all properties' | .NET 9.0 | 3,025.8012 ns | 65.7136 ns | 73.0405 ns | 3,030.1731 ns | 2,919.6998 ns | 3,167.3712 ns | ? | ? | 0.2567 | 4120 B | ? | +| 'Property modification - set all properties' | .NET 9.0 | 2,958.1106 ns | 65.5500 ns | 72.8586 ns | 2,931.3603 ns | 2,871.0808 ns | 3,147.9898 ns | ? | ? | 0.2529 | 4120 B | ? | | | | | | | | | | | | | | | -| 'ValidateOptions - valid state' | .NET 10.0 | 6.6174 ns | 0.1017 ns | 0.0951 ns | 6.5929 ns | 6.5174 ns | 6.8342 ns | ? | ? | - | - | ? | +| 'ValidateOptions - valid state' | .NET 10.0 | 7.0237 ns | 0.0981 ns | 0.0870 ns | 7.0394 ns | 6.8917 ns | 7.1845 ns | ? | ? | - | - | ? | | | | | | | | | | | | | | | -| 'ValidateOptions - valid state' | .NET 9.0 | 3.8764 ns | 0.0950 ns | 0.0933 ns | 3.8613 ns | 3.7397 ns | 4.0996 ns | ? | ? | - | - | ? | +| 'ValidateOptions - valid state' | .NET 9.0 | 3.9379 ns | 0.1053 ns | 0.1081 ns | 3.9329 ns | 3.7514 ns | 4.1705 ns | ? | ? | - | - | ? | diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkContextBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkContextBenchmark-report-github.md new file mode 100644 index 0000000..a4f5a04 --- /dev/null +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkContextBenchmark-report-github.md @@ -0,0 +1,54 @@ +``` + +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Runtime | ArgsCount | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|---------------------------------------------------- |---------- |---------- |----------:|----------:|----------:|----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| +| **'Construct BenchmarkContext with empty args'** | **.NET 10.0** | **0** | **4.3308 ns** | **0.1697 ns** | **0.1887 ns** | **4.3068 ns** | **4.0916 ns** | **4.7537 ns** | **1.00** | **0.06** | **0.0015** | **24 B** | **1.00** | +| 'Construct BenchmarkContext with null args' | .NET 10.0 | 0 | 2.9383 ns | 0.2773 ns | 0.3082 ns | 2.8849 ns | 2.6235 ns | 3.6384 ns | 0.68 | 0.07 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 10.0 | 0 | 3.7137 ns | 0.1044 ns | 0.0976 ns | 3.6983 ns | 3.5100 ns | 3.8736 ns | 0.86 | 0.04 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 10.0 | 0 | 0.6430 ns | 0.0409 ns | 0.0362 ns | 0.6344 ns | 0.5997 ns | 0.7230 ns | 0.15 | 0.01 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| 'Construct BenchmarkContext with empty args' | .NET 9.0 | 0 | 3.8301 ns | 0.2265 ns | 0.2517 ns | 3.8544 ns | 3.4316 ns | 4.3832 ns | 1.00 | 0.09 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with null args' | .NET 9.0 | 0 | 3.5669 ns | 0.1647 ns | 0.1692 ns | 3.5704 ns | 3.3070 ns | 3.9297 ns | 0.94 | 0.07 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 9.0 | 0 | 3.7700 ns | 0.2227 ns | 0.2475 ns | 3.7711 ns | 3.4919 ns | 4.4114 ns | 0.99 | 0.09 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 9.0 | 0 | 0.7330 ns | 0.0683 ns | 0.0701 ns | 0.7152 ns | 0.6434 ns | 0.8802 ns | 0.19 | 0.02 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| **'Construct BenchmarkContext with empty args'** | **.NET 10.0** | **8** | **3.1951 ns** | **0.3348 ns** | **0.3856 ns** | **3.1650 ns** | **2.6105 ns** | **3.7070 ns** | **1.01** | **0.17** | **0.0015** | **24 B** | **1.00** | +| 'Construct BenchmarkContext with null args' | .NET 10.0 | 8 | 3.2411 ns | 0.3719 ns | 0.4283 ns | 3.2930 ns | 2.5685 ns | 4.2290 ns | 1.03 | 0.18 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 10.0 | 8 | 3.1851 ns | 0.2819 ns | 0.3133 ns | 3.0243 ns | 2.7742 ns | 3.8016 ns | 1.01 | 0.16 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 10.0 | 8 | 0.6626 ns | 0.0460 ns | 0.0430 ns | 0.6569 ns | 0.6087 ns | 0.7280 ns | 0.21 | 0.03 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| 'Construct BenchmarkContext with empty args' | .NET 9.0 | 8 | 3.7102 ns | 0.1381 ns | 0.1535 ns | 3.7001 ns | 3.5006 ns | 4.0536 ns | 1.00 | 0.06 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with null args' | .NET 9.0 | 8 | 3.7332 ns | 0.1588 ns | 0.1630 ns | 3.7490 ns | 3.5074 ns | 4.1163 ns | 1.01 | 0.06 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 9.0 | 8 | 3.8434 ns | 0.2557 ns | 0.2945 ns | 3.7878 ns | 3.4957 ns | 4.5042 ns | 1.04 | 0.09 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 9.0 | 8 | 0.6815 ns | 0.0474 ns | 0.0444 ns | 0.6644 ns | 0.6268 ns | 0.7629 ns | 0.18 | 0.01 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| **'Construct BenchmarkContext with empty args'** | **.NET 10.0** | **64** | **2.8310 ns** | **0.1972 ns** | **0.2192 ns** | **2.7632 ns** | **2.6250 ns** | **3.4390 ns** | **1.01** | **0.10** | **0.0015** | **24 B** | **1.00** | +| 'Construct BenchmarkContext with null args' | .NET 10.0 | 64 | 2.8201 ns | 0.1224 ns | 0.1360 ns | 2.8519 ns | 2.6071 ns | 3.0278 ns | 1.00 | 0.08 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 10.0 | 64 | 2.9536 ns | 0.1123 ns | 0.1153 ns | 2.9485 ns | 2.7572 ns | 3.1038 ns | 1.05 | 0.08 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 10.0 | 64 | 0.6605 ns | 0.0620 ns | 0.0580 ns | 0.6445 ns | 0.5776 ns | 0.7656 ns | 0.23 | 0.03 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| 'Construct BenchmarkContext with empty args' | .NET 9.0 | 64 | 3.7801 ns | 0.2699 ns | 0.3108 ns | 3.6587 ns | 3.4339 ns | 4.3658 ns | 1.01 | 0.11 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with null args' | .NET 9.0 | 64 | 4.0339 ns | 0.3728 ns | 0.4293 ns | 3.9704 ns | 3.4332 ns | 4.9281 ns | 1.07 | 0.14 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 9.0 | 64 | 4.1926 ns | 0.3751 ns | 0.4320 ns | 4.3145 ns | 3.6149 ns | 4.8710 ns | 1.12 | 0.14 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 9.0 | 64 | 0.7207 ns | 0.0680 ns | 0.0636 ns | 0.6908 ns | 0.6453 ns | 0.8471 ns | 0.19 | 0.02 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| **'Construct BenchmarkContext with empty args'** | **.NET 10.0** | **256** | **2.7304 ns** | **0.1031 ns** | **0.1059 ns** | **2.6816 ns** | **2.5805 ns** | **2.9866 ns** | **1.00** | **0.05** | **0.0015** | **24 B** | **1.00** | +| 'Construct BenchmarkContext with null args' | .NET 10.0 | 256 | 2.7992 ns | 0.1377 ns | 0.1530 ns | 2.8273 ns | 2.5493 ns | 3.1046 ns | 1.03 | 0.07 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 10.0 | 256 | 3.0138 ns | 0.1205 ns | 0.1339 ns | 2.9849 ns | 2.8299 ns | 3.2886 ns | 1.11 | 0.06 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 10.0 | 256 | 0.6323 ns | 0.0360 ns | 0.0281 ns | 0.6335 ns | 0.5944 ns | 0.6804 ns | 0.23 | 0.01 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| 'Construct BenchmarkContext with empty args' | .NET 9.0 | 256 | 4.7236 ns | 0.1670 ns | 0.1856 ns | 4.6982 ns | 4.5087 ns | 5.1179 ns | 1.00 | 0.05 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with null args' | .NET 9.0 | 256 | 3.5940 ns | 0.1319 ns | 0.1466 ns | 3.5709 ns | 3.3897 ns | 3.9573 ns | 0.76 | 0.04 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 9.0 | 256 | 3.7935 ns | 0.1367 ns | 0.1520 ns | 3.8089 ns | 3.5272 ns | 4.0886 ns | 0.80 | 0.04 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 9.0 | 256 | 0.6915 ns | 0.0522 ns | 0.0436 ns | 0.6913 ns | 0.6360 ns | 0.8050 ns | 0.15 | 0.01 | - | - | 0.00 | diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkProgramBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkProgramBenchmark-report-github.md new file mode 100644 index 0000000..459881b --- /dev/null +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkProgramBenchmark-report-github.md @@ -0,0 +1,28 @@ +``` + +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|-------------------------------------------- |---------- |--------------:|------------:|------------:|--------------:|--------------:|--------------:|----------:|----------:|-------:|----------:|------------:| +| 'Access BuildConfiguration property' | .NET 10.0 | 0.0610 ns | 0.0436 ns | 0.0386 ns | 0.0570 ns | 0.0108 ns | 0.1273 ns | 1.72 | 2.01 | - | - | NA | +| 'Access IsDebugBuild property' | .NET 10.0 | 0.0048 ns | 0.0127 ns | 0.0106 ns | 0.0000 ns | 0.0000 ns | 0.0356 ns | 0.13 | 0.40 | - | - | NA | +| 'Check assembly debug build status' | .NET 10.0 | 2,062.5568 ns | 227.9485 ns | 243.9024 ns | 1,989.5074 ns | 1,820.6402 ns | 2,634.0309 ns | 58,090.94 | 50,285.20 | 0.5438 | 8793 B | NA | +| 'Resolve build configuration from assembly' | .NET 10.0 | 2,098.8584 ns | 223.0681 ns | 238.6804 ns | 1,976.7257 ns | 1,880.0177 ns | 2,626.4587 ns | 59,113.36 | 51,111.52 | 0.5473 | 8793 B | NA | +| 'Check entry assembly debug build status' | .NET 10.0 | 2,150.8535 ns | 68.8797 ns | 67.6491 ns | 2,123.1321 ns | 2,072.9937 ns | 2,304.4094 ns | 60,577.77 | 51,701.90 | 0.5289 | 8529 B | NA | +| 'Static property access pattern' | .NET 10.0 | 0.0582 ns | 0.0507 ns | 0.0543 ns | 0.0508 ns | 0.0000 ns | 0.1548 ns | 1.64 | 2.40 | - | - | NA | +| | | | | | | | | | | | | | +| 'Access BuildConfiguration property' | .NET 9.0 | 0.0260 ns | 0.0296 ns | 0.0277 ns | 0.0210 ns | 0.0000 ns | 0.0874 ns | ? | ? | - | - | ? | +| 'Access IsDebugBuild property' | .NET 9.0 | 0.0312 ns | 0.0285 ns | 0.0292 ns | 0.0331 ns | 0.0000 ns | 0.1032 ns | ? | ? | - | - | ? | +| 'Check assembly debug build status' | .NET 9.0 | 2,434.8425 ns | 197.4947 ns | 219.5148 ns | 2,389.7296 ns | 2,216.3000 ns | 3,040.9758 ns | ? | ? | 0.5510 | 8825 B | ? | +| 'Resolve build configuration from assembly' | .NET 9.0 | 2,256.3108 ns | 122.3223 ns | 130.8835 ns | 2,195.9803 ns | 2,166.4730 ns | 2,592.2347 ns | ? | ? | 0.5484 | 8793 B | ? | +| 'Check entry assembly debug build status' | .NET 9.0 | 2,321.5775 ns | 155.7788 ns | 166.6816 ns | 2,229.0164 ns | 2,149.6204 ns | 2,659.6258 ns | ? | ? | 0.5423 | 8561 B | ? | +| 'Static property access pattern' | .NET 9.0 | 0.2924 ns | 0.0498 ns | 0.0466 ns | 0.2807 ns | 0.2035 ns | 0.3729 ns | ? | ? | - | - | ? | diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkWorkerBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkWorkerBenchmark-report-github.md new file mode 100644 index 0000000..f02b08d --- /dev/null +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkWorkerBenchmark-report-github.md @@ -0,0 +1,26 @@ +``` + +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|---------------------------------- |---------- |--------------:|------------:|------------:|--------------:|--------------:|--------------:|-------:|--------:|-------:|-------:|----------:|------------:| +| 'Construct BenchmarkWorker' | .NET 10.0 | 4.5280 ns | 0.2018 ns | 0.2324 ns | 4.5227 ns | 4.1448 ns | 5.0388 ns | 1.00 | 0.07 | 0.0020 | - | 32 B | 1.00 | +| 'Configure services' | .NET 10.0 | 67.0518 ns | 2.4825 ns | 2.6562 ns | 66.3017 ns | 64.0598 ns | 73.2110 ns | 14.84 | 0.93 | 0.0395 | - | 624 B | 19.50 | +| 'Configure services with options' | .NET 10.0 | 1,310.1326 ns | 35.2009 ns | 39.1257 ns | 1,292.2983 ns | 1,252.4459 ns | 1,383.2906 ns | 290.06 | 16.68 | 0.5072 | 0.0961 | 7976 B | 249.25 | +| 'Access configuration' | .NET 10.0 | 0.6325 ns | 0.0491 ns | 0.0435 ns | 0.6303 ns | 0.5750 ns | 0.7115 ns | 0.14 | 0.01 | - | - | - | 0.00 | +| 'Access environment' | .NET 10.0 | 0.6581 ns | 0.0604 ns | 0.0535 ns | 0.6514 ns | 0.5793 ns | 0.7540 ns | 0.15 | 0.01 | - | - | - | 0.00 | +| | | | | | | | | | | | | | | +| 'Construct BenchmarkWorker' | .NET 9.0 | 4.5442 ns | 0.1618 ns | 0.1799 ns | 4.4841 ns | 4.2818 ns | 4.8308 ns | 1.00 | 0.05 | 0.0020 | - | 32 B | 1.00 | +| 'Configure services' | .NET 9.0 | 78.0880 ns | 1.6229 ns | 1.8038 ns | 78.4799 ns | 75.0539 ns | 81.4879 ns | 17.21 | 0.76 | 0.0396 | - | 624 B | 19.50 | +| 'Configure services with options' | .NET 9.0 | 1,827.5207 ns | 215.5665 ns | 248.2466 ns | 1,916.0051 ns | 1,398.1489 ns | 2,121.1524 ns | 402.76 | 55.60 | 0.5046 | 0.1235 | 7944 B | 248.25 | +| 'Access configuration' | .NET 9.0 | 0.6918 ns | 0.0668 ns | 0.0714 ns | 0.6700 ns | 0.6141 ns | 0.8275 ns | 0.15 | 0.02 | - | - | - | 0.00 | +| 'Access environment' | .NET 9.0 | 0.6554 ns | 0.0526 ns | 0.0492 ns | 0.6580 ns | 0.5846 ns | 0.7320 ns | 0.14 | 0.01 | - | - | - | 0.00 | From f889f3f0979135577ad15e146a7723cc929c8736 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Fri, 12 Dec 2025 00:55:01 +0100 Subject: [PATCH 29/32] :art: update readme with folder structure and usage examples --- .../README.md | 41 +++++++++++ README.md | 70 +++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md b/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md index 13a9620..88556e8 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md @@ -12,6 +12,45 @@ It removes unnecessary ceremony while embracing best practices from other consum At its heart, the package is **free, flexible, and crafted to extend and empower your agile codebelt**. +## Folder Structure + +The folder structure promoted by **Codebelt.Extensions.BenchmarkDotNet** follows the same architectural principles commonly used for test projects—while remaining purpose-built for benchmarking. + +At the solution level, benchmarks are treated as a first-class concern, clearly separated from tooling and output artifacts. + +- **tuning** contains all benchmark projects (e.g. `*.Benchmarks`), in the same way that a `test` folder typically contains `*.Tests` projects, +- **tooling** hosts the executable console application responsible for discovering and running benchmarks, +- **reports** captures benchmark results and generated artifacts, separated from source code and tooling concerns. + +This separation enforces a clean boundary between benchmark definition, execution, and output, making benchmark suites easier to scale, automate, and reason about. + +### Example Layout + +```text +Repository Root +│ +├─ reports +│ └─ tuning +│ └─ github +│ └─ MyLibrary.ExampleBenchmarks-report-github.md +│ +├─ src +│ └─ MyLibrary +│ +├─ test +│ └─ MyLibrary.Tests +│ └─ ExampleTest.cs +│ +├─ tooling +│ └─ benchmark-runner +│ └─ Program.cs +│ +└─ tuning + └─ MyLibrary.Benchmarks + └─ ExampleBenchmark.cs +``` + + ## CSharp Example Benchmarks are executed using a Generic Host–based bootstrap model, allowing BenchmarkDotNet to participate in a fully managed application lifecycle with dependency injection, configuration, and logging. @@ -31,6 +70,8 @@ var host = hostBuilder.Build(); host.Run(); ``` +The folder structure is based o + ## Related Packages * [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) 📦 diff --git a/README.md b/README.md index 478e789..52585e3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,76 @@ Your versatile BenchmarkDotNet companion for modern development with `.NET 9` an It is, by heart, free, flexible and built to extend and boost your agile codebelt. +## Concept + +The Extensions for BenchmarkDotNet API by Codebelt is designed to bring clarity, structure, and consistency to BenchmarkDotNet projects. + +### Folder Structure + +The folder structure promoted by **Codebelt.Extensions.BenchmarkDotNet** follows the same architectural principles commonly used for test projects—while remaining purpose-built for benchmarking. + +At the solution level, benchmarks are treated as a first-class concern, clearly separated from tooling and output artifacts. + +- **tuning** contains all benchmark projects (e.g. `*.Benchmarks`), in the same way that a `test` folder typically contains `*.Tests` projects, +- **tooling** hosts the executable console application responsible for discovering and running benchmarks, +- **reports** captures benchmark results and generated artifacts, separated from source code and tooling concerns. + +This separation enforces a clean boundary between benchmark definition, execution, and output, making benchmark suites easier to scale, automate, and reason about. + +### Layout Example + +```text +Repository Root +│ +├─ reports +│ └─ tuning +│ └─ github +│ └─ MyLibrary.ExampleBenchmarks-report-github.md +│ +├─ src +│ └─ MyLibrary +│ +├─ test +│ └─ MyLibrary.Tests +│ └─ ExampleTest.cs +│ +├─ tooling +│ └─ benchmark-runner +│ └─ Program.cs +│ +└─ tuning + └─ MyLibrary.Benchmarks + └─ ExampleBenchmark.cs +``` + +### Codebelt.Extensions.BenchmarkDotNet.Console Example + +Benchmarks are executed through a console-hosted Generic Host, allowing full participation in dependency injection, configuration, logging and more. + +```csharp +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using Codebelt.Extensions.BenchmarkDotNet.Console; + +public class Program +{ + public static void Main(string[] args) + { + BenchmarkProgram.Run(args, o => + { + o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; + o.ConfigureBenchmarkDotNet(c => + { + var slimJob = BenchmarkWorkspaceOptions.Slim; + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) + .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + }); + }); + } +} +``` + ## 📚 Documentation Full documentation (generated by [DocFx](https://github.com/dotnet/docfx)) located here: https://benchmarkdotnet.codebelt.net/ From 47882072ba83800d787f5bdf1487d8c9e0057ac1 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Fri, 12 Dec 2025 00:56:58 +0100 Subject: [PATCH 30/32] :art: fix os matrix formatting in ci pipeline configuration --- .github/workflows/ci-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 2f4b766..2cf5ce3 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -45,7 +45,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-24.04, windows-2025, ubuntu-24-04-arm, windows-11-arm] + os: [ubuntu-24.04, windows-2025, ubuntu-24.04-arm, windows-11-arm] configuration: [Debug, Release] uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: From 0af72cd36e4b5e4c317e9f8c50845f34ba1cebb5 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Fri, 12 Dec 2025 01:01:01 +0100 Subject: [PATCH 31/32] :pencil2: fix typo in benchmarkdotnet project/repo names in ci-pipeline --- .github/workflows/ci-pipeline.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 2cf5ce3..3cc7504 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -61,7 +61,7 @@ jobs: uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 with: organization: geekle - projectKey: bemchmarkdotnet + projectKey: benchmarkdotnet version: ${{ needs.build.outputs.version }} secrets: inherit @@ -70,7 +70,7 @@ jobs: needs: [build, test] uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 with: - repository: codebeltnet/bemchmarkdotnet + repository: codebeltnet/benchmarkdotnet secrets: inherit codeql: From d89b6b14418d9b012addee220b7f186b26d8d82b Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Fri, 12 Dec 2025 01:04:43 +0100 Subject: [PATCH 32/32] :adhesive_bandage: handle null GetEntryAssembly in BenchmarkProgram ctor --- .../BenchmarkProgram.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs index d42aab9..dc35e07 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs @@ -16,7 +16,7 @@ public class BenchmarkProgram : ConsoleProgram { static BenchmarkProgram() { - var isDebugBuild = Decorator.Enclose(Assembly.GetEntryAssembly()).IsDebugBuild(); + var isDebugBuild = Decorator.Enclose(Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).IsDebugBuild(); BuildConfiguration = isDebugBuild ? "Debug" : "Release"; IsDebugBuild = isDebugBuild; }