diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 12f8f3c..414681e 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,4 +1,4 @@
-# Contributing to `Classlibrary1`
+# Contributing to `BenchmarkDotNet Extensions by Codebelt`
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
@@ -48,5 +48,5 @@ That is, in pursuit of the items on the left we have found the items on the righ
[Manifesto for Software Craftsmanship](https://manifesto.softwarecraftsmanship.org/) is the originator of this text.
## License
-By contributing to `ClassLibrary1`, you agree that your contributions will be licensed
+By contributing to `BenchmarkDotNet Extensions by Codebelt`, you agree that your contributions will be licensed
under the MIT license.
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 9ef3035..27107cc 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -149,6 +149,37 @@ namespace Codebelt.Extensions.BenchmarkDotNet
- Before overriding methods, verify that the method is virtual or abstract; this rule also applies to mocks.
- Never mock IMarshaller; always use a new instance of JsonMarshaller.
+## 9. Avoid `InternalsVisibleTo` in Tests
+
+- **Do not** use `InternalsVisibleTo` to access internal types or members from test projects.
+- Prefer **indirect testing via public APIs** that depend on the internal implementation (public facades, public extension methods, or other public entry points).
+
+### Preferred Pattern
+
+**Pattern name:** Public Facade Testing (also referred to as *Public API Proxy Testing*)
+
+**Description:**
+Internal classes and methods must be validated by exercising the public API that consumes them. Tests should assert observable behavior exposed by the public surface rather than targeting internal implementation details directly.
+
+### Example Mapping
+
+- **Internal helper:** `DelimitedString` (internal static class)
+- **Public API:** `TestOutputHelperExtensions.WriteLines()` (public extension method)
+- **Test strategy:** Write tests for `WriteLines()` and verify its public behavior. The internal call to `DelimitedString.Create()` is exercised implicitly.
+
+### Benefits
+
+- Avoids exposing internal types to test assemblies.
+- Ensures tests reflect real-world usage patterns.
+- Maintains strong encapsulation and a clean public API.
+- Tests remain resilient to internal refactoring as long as public behavior is preserved.
+
+### When to Apply
+
+- Internal logic is fully exercised through existing public APIs.
+- Public entry points provide sufficient coverage of internal code paths.
+- The internal implementation exists solely as a helper or utility for public-facing functionality.
+
---
description: 'Writing Performance Tests in Codebelt.Extensions.BenchmarkDotNet'
applyTo: "tuning/**, **/*Benchmark*.cs"
diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt
index 59e6169..66033c2 100644
--- a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt
+++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt
@@ -1,6 +1,17 @@
+Version: 1.1.0
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
+# Improvements
+- EXTENDED BenchmarkProgram class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to support an optional service configurator delegate for customizing the IServiceCollection during host building
+- EXTENDED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to support skipping benchmarks that already have generated reports based on the BenchmarkWorkspaceOptions.SkipBenchmarksWithReports property
+- CHANGED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace to conditionally suppress console status messages in services based on whether you are in a debugging session or not
+
Version: 1.0.0
Availability: .NET 10 and .NET 9
-
+
# New Features
- ADDED BenchmarkContext class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that represents the command-line context for a benchmark run
- ADDED BenchmarkProgram class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that provides the main entry point for hosting and running benchmarks using BenchmarkDotNet
diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/icon.png b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/icon.png
index 0b304b4..d3af8d6 100644
Binary files a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/icon.png and b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/icon.png differ
diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt
index 42108fa..ed4e30e 100644
--- a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt
+++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt
@@ -1,6 +1,19 @@
+Version: 1.1.0
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
+# Bug Fixes
+- FIXED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace so it no longer relies on Danish culture
+
+# Improvements
+- EXTENDED BenchmarkWorkspace class in the Codebelt.Extensions.BenchmarkDotNet namespace with two new static methods; GetReportsResultsPath and GetReportsTuningPath for retrieving the paths to the reports results and tuning directories respectively
+- EXTENDED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace to include one new property; SkipBenchmarksWithReports that indicates whether benchmarks that already have generated reports should be skipped during execution
+
Version: 1.0.0
Availability: .NET 10 and .NET 9
-
+
# New Features
- ADDED BenchmarkWorkspace class in the Codebelt.Extensions.BenchmarkDotNet namespace that provides a default implementation of IBenchmarkWorkspace for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet
- ADDED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace that specifies configuration options that is related to the BenchmarkWorkspace class
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f41a179..732f847 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder.
+## [1.1.0] - 2025-12-14
+
+### Changed
+
+- `BenchmarkWorkspace` class in the Codebelt.Extensions.BenchmarkDotNet namespace was extended with two new static methods; `GetReportsResultsPath` and `GetReportsTuningPath` for retrieving the paths to the reports results and tuning directories respectively,
+- `BenchmarkWorkspaceOptions` class in the Codebelt.Extensions.BenchmarkDotNet namespace was extended with an additional new property; `SkipBenchmarksWithReports` that indicates whether benchmarks that already have generated reports should be skipped during execution,
+- `BenchmarkProgram` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace was extended to support an optional service configurator delegate for customizing the `IServiceCollection` during host building,
+- `BenchmarkWorker` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace was extended to support skipping benchmarks that already have generated reports based on the `BenchmarkWorkspaceOptions.SkipBenchmarksWithReports` property,
+- `BenchmarkWorker` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace was changed to conditionally suppress console status messages in services based on whether you are in a debugging session or not.
+
+### Fixed
+
+- `BenchmarkWorkspaceOptions` class in the Codebelt.Extensions.BenchmarkDotNet namespace was fixed so it no longer relies on Danish culture.
+
## [1.0.0] - 2025-12-12
This is the initial stable release of the `Codebelt.Extensions.BenchmarkDotNet` and `Codebelt.Extensions.BenchmarkDotNet.Console` packages.
diff --git a/Directory.Build.props b/Directory.Build.props
index 1516ab1..9f59c55 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -6,7 +6,7 @@
$(MSBuildProjectDirectory.ToLower().StartsWith('$(MSBuildThisFileDirectory.ToLower())tooling'))
$([MSBuild]::IsOSPlatform('Linux'))
$([MSBuild]::IsOSPlatform('Windows'))
- true
+ true
false
..\..\.nuget\$(MSBuildProjectName)\PackageReleaseNotes.txt
latest
diff --git a/Directory.Packages.props b/Directory.Packages.props
index e454e68..3a92f84 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,7 +7,7 @@
-
+
diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs
index dc35e07..e71b672 100644
--- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs
+++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs
@@ -43,7 +43,21 @@ static BenchmarkProgram()
///
public static void Run(string[] args, Action setup = null)
{
- Run(args, setup);
+ Run(args, null, setup);
+ }
+
+ ///
+ /// Runs benchmarks using the default implementation.
+ ///
+ /// The command-line arguments passed to the application.
+ /// The delegate that will be invoked to configure additional services in the .
+ /// The which may be configured.
+ ///
+ /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks.
+ ///
+ public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null)
+ {
+ Run(args, serviceConfigurator, setup);
}
///
@@ -56,14 +70,30 @@ public static void Run(string[] args, Action setup =
/// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks.
///
public static void Run(string[] args, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace
+ {
+ Run(args, null, setup);
+ }
+
+ ///
+ /// Runs benchmarks using a custom implementation of .
+ ///
+ /// The type of the workspace that implements .
+ /// The command-line arguments passed to the application.
+ /// The delegate that will be invoked to configure additional services in the .
+ /// The which may be configured.
+ ///
+ /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks.
+ ///
+ public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace
{
var hostBuilder = CreateHostBuilder(args);
hostBuilder.ConfigureServices(services =>
{
services.AddSingleton(new BenchmarkContext(args));
services.AddBenchmarkWorkspace(setup);
+ serviceConfigurator?.Invoke(services);
});
- var host = hostBuilder.Build();
+ using var host = hostBuilder.Build();
host.Run();
}
}
diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs
index e0e0f23..fb2ca5b 100644
--- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs
+++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs
@@ -4,8 +4,12 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
+using System.IO;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Filters;
namespace Codebelt.Extensions.BenchmarkDotNet.Console
{
@@ -33,7 +37,7 @@ public BenchmarkWorker(IConfiguration configuration, IHostEnvironment environmen
///
public override void ConfigureServices(IServiceCollection services)
{
- services.Configure(o => o.SuppressStatusMessages = true);
+ services.Configure(o => o.SuppressStatusMessages = !BenchmarkProgram.IsDebugBuild);
}
///
@@ -53,6 +57,34 @@ public override Task RunAsync(IServiceProvider serviceProvider, CancellationToke
var assemblies = workspace.LoadBenchmarkAssemblies();
var context = serviceProvider.GetRequiredService();
+ if (options.SkipBenchmarksWithReports)
+ {
+ var benchmarkTypes = assemblies.SelectMany(a => a.GetTypes().Where(t => t.Name.EndsWith("Benchmark", StringComparison.Ordinal))).ToList();
+ options.ConfigureBenchmarkDotNet(c =>
+ {
+ var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options);
+ if (!Directory.Exists(tuningPath)) { return c; }
+ var reports = Directory.EnumerateFiles(tuningPath);
+ foreach (var report in reports)
+ {
+ var filename = Path.GetFileNameWithoutExtension(report);
+ var potentialTypeFullName = filename.Split('-').FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(potentialTypeFullName)) { continue; }
+
+ var potentialTypeName = potentialTypeFullName.Split('.').LastOrDefault();
+ if (string.IsNullOrWhiteSpace(potentialTypeName)) { continue; }
+
+ var matchingType = benchmarkTypes.FirstOrDefault(t => t.Name.Equals(potentialTypeName, StringComparison.OrdinalIgnoreCase));
+
+ if (matchingType != null)
+ {
+ c = c.AddFilter(new SimpleFilter(bc => bc.Descriptor.Type != matchingType));
+ }
+ }
+ return c;
+ });
+ }
+
try
{
if (context.Args.Length == 0)
diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs
index d4d0263..58a2240 100644
--- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs
+++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs
@@ -21,6 +21,34 @@ public sealed class BenchmarkWorkspace : IBenchmarkWorkspace
private static readonly Lock AssemblyResolverLock = new();
private static Dictionary _assemblyLookup = new(StringComparer.OrdinalIgnoreCase);
+ ///
+ /// Gets the path to the BenchmarkDotNet results directory
+ ///
+ /// The which configures repository paths, build modes and BenchmarkDotNet configuration.
+ /// A string containing the full path to the 'results' directory within the artifacts path.
+ ///
+ /// This path is constructed from the configured and the "results" subdirectory.
+ ///
+ public static string GetReportsResultsPath(BenchmarkWorkspaceOptions options)
+ {
+ Validator.ThrowIfInvalidOptions(options);
+ return Path.Combine(options.Configuration.ArtifactsPath, "results");
+ }
+
+ ///
+ /// Gets the path to the tuning folder where final benchmark reports are stored.
+ ///
+ /// The which configures repository paths, build modes and BenchmarkDotNet configuration.
+ /// A string representing the full path to the tuning reports directory.
+ ///
+ /// This path is constructed from the configured and the .
+ ///
+ public static string GetReportsTuningPath(BenchmarkWorkspaceOptions options)
+ {
+ Validator.ThrowIfInvalidOptions(options);
+ return Path.Combine(options.Configuration.ArtifactsPath, options.RepositoryTuningFolder);
+ }
+
///
/// Initializes a new instance of the class with the specified options.
///
@@ -74,9 +102,7 @@ public Assembly[] LoadBenchmarkAssemblies()
///
public void PostProcessArtifacts()
{
- var reportsResultsPath = Path.Combine(_options.Configuration.ArtifactsPath, "results");
- var reportsTuningPath = Path.Combine(_options.Configuration.ArtifactsPath, _options.RepositoryTuningFolder);
- CleanupResults(reportsResultsPath, reportsTuningPath);
+ CleanupResults(GetReportsResultsPath(_options), GetReportsTuningPath(_options));
}
private static IEnumerable LoadAssemblies(string repositoryPath, string targetFrameworkMoniker, string benchmarkProjectSuffix, string repositoryTuningFolder, bool useDebugBuild)
@@ -188,11 +214,10 @@ private static void CleanupResults(string reportsResultsPath, string reportsTuni
Directory.CreateDirectory(reportsTuningPath);
- foreach (var file in Directory.GetFiles(reportsResultsPath).Where(s => !s.EndsWith(".lock")))
+ foreach (var file in Directory.GetFiles(reportsResultsPath).Where(f => !Path.GetFileNameWithoutExtension(f).StartsWith("BenchmarkRun", StringComparison.Ordinal)))
{
var targetFile = Path.Combine(reportsTuningPath, Path.GetFileName(file));
- File.Delete(targetFile);
- File.Move(file, targetFile);
+ File.Move(file, targetFile, true);
}
Directory.Delete(reportsResultsPath, recursive: true);
diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs
index 3321d0a..e4f3201 100644
--- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs
+++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs
@@ -55,6 +55,10 @@ namespace Codebelt.Extensions.BenchmarkDotNet;
///
/// false
///
+/// -
+///
+/// false
+///
///
///
public class BenchmarkWorkspaceOptions : IValidatableParameterObject, IPostConfigurableParameterObject
@@ -87,7 +91,6 @@ public class BenchmarkWorkspaceOptions : IValidatableParameterObject, IPostConfi
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.
@@ -147,6 +150,14 @@ public BenchmarkWorkspaceOptions()
/// true to allow Debug builds for benchmarks; otherwise, false. Default is false.
///
public bool AllowDebugBuild { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether benchmarks that already have generated reports should be skipped during execution.
+ ///
+ ///
+ /// true to skip benchmarks with existing reports; otherwise, false. Default is false.
+ ///
+ public bool SkipBenchmarksWithReports { get; set; }
///
/// Finalizes the configured options before use.
@@ -221,7 +232,7 @@ private static ManualConfig GetDefaultConfiguration()
.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
+ .WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(36)); // the default is 20 and trims too aggressively some benchmark results
config.Options = ConfigOptions.DisableLogFile;
return config;
}
diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs
index 0e52f8d..d89dca7 100644
--- a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs
+++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs
@@ -57,6 +57,13 @@ public void BuildConfiguration_ShouldBeDebug_WhenCompiledInDebugMode()
return;
}
+ if (entryAssembly.FullName.Contains("ReSharper", StringComparison.OrdinalIgnoreCase))
+ {
+ // Skip test if running under ReSharper
+ TestOutput.WriteLine("Running under ReSharper, skipping test");
+ return;
+ }
+
// Act
var buildConfiguration = BenchmarkProgram.BuildConfiguration;
@@ -82,6 +89,13 @@ public void IsDebugBuild_ShouldBeTrue_WhenCompiledInDebugMode()
return;
}
+ if (entryAssembly.FullName.Contains("ReSharper", StringComparison.OrdinalIgnoreCase))
+ {
+ // Skip test if running under ReSharper
+ TestOutput.WriteLine("Running under ReSharper, skipping test");
+ return;
+ }
+
// Act
var isDebugBuild = BenchmarkProgram.IsDebugBuild;
diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs
index 9a4f9f4..ed4ec63 100644
--- a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs
+++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs
@@ -161,7 +161,16 @@ public void ConfigureServices_ShouldSuppressStatusMessages()
// Assert
Assert.NotNull(options);
- Assert.True(options.SuppressStatusMessages);
+
+ if (BenchmarkProgram.IsDebugBuild)
+ {
+ Assert.False(options.SuppressStatusMessages);
+ }
+ else
+ {
+ Assert.True(options.SuppressStatusMessages);
+ }
+
TestOutput.WriteLine($"ConsoleLifetimeOptions.SuppressStatusMessages = {options.SuppressStatusMessages}");
}
diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs
index afa9657..5022c6f 100644
--- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs
+++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs
@@ -583,20 +583,6 @@ public void Configuration_ShouldHaveCustomSummaryStyle()
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()
{
@@ -646,4 +632,27 @@ public void Configuration_ShouldHaveSlimJobAsDefault()
TestOutput.WriteLine($"Default Job - WarmupCount: {jobs[0].Run.WarmupCount}");
}
+
+ [Fact]
+ public void SkipBenchmarksWithReports_ShouldDefaultToFalse()
+ {
+ // Arrange & Act
+ var options = new BenchmarkWorkspaceOptions();
+
+ // Assert
+ Assert.False(options.SkipBenchmarksWithReports);
+ }
+
+ [Fact]
+ public void SkipBenchmarksWithReports_ShouldBeSettable()
+ {
+ // Arrange
+ var options = new BenchmarkWorkspaceOptions();
+
+ // Act
+ options.SkipBenchmarksWithReports = true;
+
+ // Assert
+ Assert.True(options.SkipBenchmarksWithReports);
+ }
}
diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs
index 9f07e74..ab420a5 100644
--- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs
+++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs
@@ -627,4 +627,178 @@ public void LoadBenchmarkAssemblies_ShouldHandleAssemblyLoadFailuresGracefully()
}
}
}
+
+ [Fact]
+ public void GetReportsResultsPath_ShouldReturnCorrectPath_WhenOptionsAreValid()
+ {
+ // Arrange
+ var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ try
+ {
+ Directory.CreateDirectory(tempPath);
+ var artifactsPath = Path.Combine(tempPath, "artifacts");
+ var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath);
+ var options = new BenchmarkWorkspaceOptions
+ {
+ RepositoryPath = tempPath,
+ Configuration = config
+ };
+
+ // Act
+ var resultsPath = BenchmarkWorkspace.GetReportsResultsPath(options);
+
+ // Assert
+ var expectedPath = Path.Combine(artifactsPath, "results");
+ Assert.Equal(expectedPath, resultsPath);
+
+ TestOutput.WriteLine($"Results path: {resultsPath}");
+ }
+ finally
+ {
+ if (Directory.Exists(tempPath))
+ {
+ Directory.Delete(tempPath, true);
+ }
+ }
+ }
+
+ [Fact]
+ public void GetReportsResultsPath_ShouldThrowArgumentException_WhenOptionsAreInvalid()
+ {
+ // Arrange
+ var options = new BenchmarkWorkspaceOptions
+ {
+ RepositoryPath = null
+ };
+
+ // Act & Assert
+ Assert.Throws(() => BenchmarkWorkspace.GetReportsResultsPath(options));
+ }
+
+ [Fact]
+ public void GetReportsResultsPath_ShouldThrowArgumentNullException_WhenOptionsIsNull()
+ {
+ // Arrange
+ BenchmarkWorkspaceOptions options = null;
+
+ // Act & Assert
+ Assert.Throws(() => BenchmarkWorkspace.GetReportsResultsPath(options));
+ }
+
+ [Fact]
+ public void GetReportsTuningPath_ShouldReturnCorrectPath_WhenOptionsAreValid()
+ {
+ // Arrange
+ var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ try
+ {
+ Directory.CreateDirectory(tempPath);
+ var artifactsPath = Path.Combine(tempPath, "artifacts");
+ var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath);
+ var options = new BenchmarkWorkspaceOptions
+ {
+ RepositoryPath = tempPath,
+ Configuration = config,
+ RepositoryTuningFolder = "tuning"
+ };
+
+ // Act
+ var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options);
+
+ // Assert
+ var expectedPath = Path.Combine(artifactsPath, "tuning");
+ Assert.Equal(expectedPath, tuningPath);
+
+ TestOutput.WriteLine($"Tuning path: {tuningPath}");
+ }
+ finally
+ {
+ if (Directory.Exists(tempPath))
+ {
+ Directory.Delete(tempPath, true);
+ }
+ }
+ }
+
+ [Fact]
+ public void GetReportsTuningPath_ShouldThrowArgumentException_WhenOptionsAreInvalid()
+ {
+ // Arrange
+ var options = new BenchmarkWorkspaceOptions
+ {
+ RepositoryPath = null
+ };
+
+ // Act & Assert
+ Assert.Throws(() => BenchmarkWorkspace.GetReportsTuningPath(options));
+ }
+
+ [Fact]
+ public void GetReportsTuningPath_ShouldThrowArgumentNullException_WhenOptionsIsNull()
+ {
+ // Arrange
+ BenchmarkWorkspaceOptions options = null;
+
+ // Act & Assert
+ Assert.Throws(() => BenchmarkWorkspace.GetReportsTuningPath(options));
+ }
+
+ [Fact]
+ public void GetReportsTuningPath_ShouldUseCustomTuningFolder_WhenSpecified()
+ {
+ // Arrange
+ var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ try
+ {
+ Directory.CreateDirectory(tempPath);
+ var artifactsPath = Path.Combine(tempPath, "artifacts");
+ var customTuningFolder = "custom-tuning";
+ var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath);
+ var options = new BenchmarkWorkspaceOptions
+ {
+ RepositoryPath = tempPath,
+ Configuration = config,
+ RepositoryTuningFolder = customTuningFolder
+ };
+
+ // Act
+ var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options);
+
+ // Assert
+ var expectedPath = Path.Combine(artifactsPath, customTuningFolder);
+ Assert.Equal(expectedPath, tuningPath);
+ Assert.Contains(customTuningFolder, tuningPath);
+
+ TestOutput.WriteLine($"Custom tuning path: {tuningPath}");
+ }
+ finally
+ {
+ if (Directory.Exists(tempPath))
+ {
+ Directory.Delete(tempPath, true);
+ }
+ }
+ }
+
+ [Fact]
+ public void GetReportsResultsPath_AndGetReportsTuningPath_ShouldReturnDifferentPaths()
+ {
+ // Arrange
+ var options = new BenchmarkWorkspaceOptions
+ {
+ AllowDebugBuild = IsDebugBuild
+ };
+
+ // Act
+ var resultsPath = BenchmarkWorkspace.GetReportsResultsPath(options);
+ var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options);
+
+ // Assert
+ Assert.NotEqual(resultsPath, tuningPath);
+ Assert.Contains("results", resultsPath);
+ Assert.Contains("tuning", tuningPath);
+
+ TestOutput.WriteLine($"Results path: {resultsPath}");
+ TestOutput.WriteLine($"Tuning path: {tuningPath}");
+ }
}
diff --git a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs
index ef47eb4..d639e65 100644
--- a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs
+++ b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs
@@ -12,6 +12,7 @@ public static void Main(string[] args)
BenchmarkProgram.Run(args, o =>
{
o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild;
+ o.SkipBenchmarksWithReports = true;
o.ConfigureBenchmarkDotNet(c =>
{
var slimJob = BenchmarkWorkspaceOptions.Slim;