diff --git a/.docfx/includes/availability-modern.md b/.docfx/includes/availability-modern.md index 544cb79..a13a3f6 100644 --- a/.docfx/includes/availability-modern.md +++ b/.docfx/includes/availability-modern.md @@ -1 +1 @@ -Availability: .NET 10 and .NET 9.0 \ No newline at end of file +Availability: .NET 11 and .NET 10 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9520cf3..a14d35f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -5,7 +5,7 @@ This repository is part of the Codebelt .NET library estate. The instructions be ## Before you start - Read the repository `README.md` and open an issue before starting a non-trivial feature or behavioral change. -- Use an installed .NET SDK that can build the target frameworks listed below. This repository currently targets: **net10.0, net9.0**. +- Use an installed .NET SDK that can build the target frameworks listed below. This repository currently targets: **net11.0, net10.0**. - The solution is `Codebelt.Extensions.BenchmarkDotNet.slnx`. Central package versions are maintained in `Directory.Packages.props`. - The shared build behavior is in `Directory.Build.props` and `Directory.Build.targets`; repository-specific TFMs, package references and metadata remain local to this library. diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 37b90a2..701a3db 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -59,6 +59,7 @@ jobs: configuration: [Debug, Release] uses: codebeltnet/jobs-dotnet-build/.github/workflows/default.yml@v3 with: + include-preview: true configuration: ${{ matrix.configuration }} strong-name-key-filename: ${{ needs.init.outputs.strong-name-key-filename }} build-switches: ${{ needs.init.outputs.build-switches }} @@ -74,6 +75,7 @@ jobs: configuration: [Debug, Release] uses: codebeltnet/jobs-dotnet-pack/.github/workflows/default.yml@v3 with: + include-preview: true configuration: ${{ matrix.configuration }} version: ${{ needs.build.outputs.version }} download-build-artifact-pattern: build-${{ matrix.configuration }}-X64 @@ -88,6 +90,7 @@ jobs: arch: [X64, ARM64] uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: + include-preview: true runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} configuration: ${{ matrix.configuration }} build-switches: -p:SkipSignAssembly=true @@ -105,6 +108,7 @@ jobs: arch: [X64, ARM64] uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: + include-preview: true runs-on: ${{ matrix.arch == 'ARM64' && 'windows-11-arm' || 'windows-2025' }} configuration: ${{ matrix.configuration }} build-switches: -p:SkipSignAssembly=true @@ -123,6 +127,7 @@ jobs: configuration: [Debug, Release] uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: + include-preview: true runs-on: ${{ matrix.arch == 'ARM64' && 'macos-26' || 'macos-26-intel' }} configuration: ${{ matrix.configuration }} build-switches: -p:SkipSignAssembly=true @@ -180,6 +185,7 @@ jobs: needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 with: + include-preview: true organization: geekle projectKey: benchmarkdotnet version: ${{ needs.build.outputs.version }} @@ -199,6 +205,8 @@ jobs: name: call-codeql needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3 + with: + include-preview: true permissions: security-events: write diff --git a/.github/workflows/service-update.yml b/.github/workflows/service-update.yml index f9a235e..f55bbc4 100644 --- a/.github/workflows/service-update.yml +++ b/.github/workflows/service-update.yml @@ -31,6 +31,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + fetch-tags: true - name: Resolve trigger inputs id: trigger @@ -42,13 +43,68 @@ jobs: - name: Determine new version for this repo id: newver + shell: bash run: | - CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1) - NEW=$(echo "$CURRENT" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}') - BRANCH="v${NEW}/service-update" - echo "current=$CURRENT" >> $GITHUB_OUTPUT - echo "new=$NEW" >> $GITHUB_OUTPUT - echo "branch=$BRANCH" >> $GITHUB_OUTPUT + python3 - <<'PY' + import os + import re + import subprocess + import sys + + tag_pattern = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") + tags = subprocess.check_output(["git", "tag", "--list"], text=True).splitlines() + stable_tags = [] + for tag in tags: + match = tag_pattern.fullmatch(tag) + if match: + version = tuple(int(part) for part in match.groups()) + stable_tags.append((version, tag)) + + if not stable_tags: + print("::error::No stable vMAJOR.MINOR.PATCH Git tag was found.") + sys.exit(1) + + _, latest_tag = max(stable_tags, key=lambda item: item[0]) + latest_version = latest_tag[1:] + major, minor, patch = (int(part) for part in latest_version.split(".")) + next_version = f"{major}.{minor}.{patch + 1}" + + with open("CHANGELOG.md", encoding="utf-8") as changelog_file: + changelog = changelog_file.read() + + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - ([^\r\n]+)[ \t]*$", changelog) + if not heading: + print("::error::CHANGELOG.md has no versioned heading to compare with Git tags.") + sys.exit(1) + + current_version, current_status = heading.groups() + current_status = current_status.strip() + if current_status.upper() != "TBD" and not re.fullmatch(r"\d{4}-\d{2}-\d{2}", current_status): + print(f"::error::Unsupported CHANGELOG.md version status: {current_status}") + sys.exit(1) + + if current_version == next_version and current_status.upper() == "TBD": + # Reuse an already pending next release, such as [5.0.13] - TBD. + new_version = next_version + elif current_version == latest_version and current_status.upper() != "TBD": + # The changelog is at the latest released tag; start the next patch release. + new_version = next_version + else: + print( + f"::error::CHANGELOG.md starts at {current_version} ({current_status}), " + f"but the latest stable Git tag is {latest_tag}. Expected either " + f"[{latest_version}] with a release date or [{next_version}] - TBD." + ) + sys.exit(1) + + branch = f"v{new_version}/service-update" + print(f"Latest stable Git tag: {latest_tag}") + print(f"Latest CHANGELOG.md version: {current_version} ({current_status})") + print(f"Service update version: {new_version}") + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"current={current_version}\n") + output.write(f"new={new_version}\n") + output.write(f"branch={branch}\n") - name: Generate codebelt-aicia token id: app-token @@ -69,7 +125,7 @@ jobs: NEW="${{ steps.newver.outputs.new }}" for f in .nuget/*/PackageReleaseNotes.txt; do [ -f "$f" ] || continue - TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //' || echo ".NET 10, .NET 9 and .NET Standard 2.0") + TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //' || echo ".NET 11 and .NET 10") ENTRY="Version: ${NEW}\nAvailability: ${TFM}\n\n# ALM\n- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)\n\n" { printf "$ENTRY"; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f" done @@ -81,14 +137,27 @@ jobs: from datetime import date new_ver = os.environ['NEW_VERSION'] today = date.today().isoformat() - entry = f"## [{new_ver}] - {today}\n\nThis is a service update that focuses on package dependencies.\n\n" - with open("CHANGELOG.md") as f: + service_note = "This release also includes a service update focused on package dependencies." + with open("CHANGELOG.md", encoding="utf-8") as f: content = f.read() - idx = content.find("## [") - content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry) - with open("CHANGELOG.md", "w") as f: + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - (TBD|\d{4}-\d{2}-\d{2})[ \t]*$", content) + if heading and heading.group(1) == new_ver: + if heading.group(2) == "TBD": + content = content[:heading.start(2)] + today + content[heading.end(2):] + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - \d{4}-\d{2}-\d{2}[ \t]*$", content) + section_end = re.search(r"(?m)^## \[", content[heading.end():]) + end = heading.end() + section_end.start() if section_end else len(content) + if service_note not in content[heading.end():end]: + remainder = content[heading.end():] + content = content[:heading.end()] + "\n\n" + service_note + "\n\n" + remainder.lstrip("\r\n") + print(f"Updated existing CHANGELOG entry for v{new_ver}") + else: + entry = f"## [{new_ver}] - {today}\n\nThis is a service update that focuses on package dependencies.\n\n" + idx = content.find("## [") + content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry) + print(f"Added CHANGELOG entry for v{new_ver}") + with open("CHANGELOG.md", "w", encoding="utf-8") as f: f.write(content) - print(f"CHANGELOG updated for v{new_ver}") EOF env: NEW_VERSION: ${{ steps.newver.outputs.new }} diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt index 3622691..02121e0 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt @@ -1,33 +1,39 @@ -Version: 1.3.5 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -Version: 1.3.4 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -Version: 1.3.3 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -Version: 1.3.2 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -Version: 1.3.1 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - +Version: 2.0.0 +Availability: .NET 11 and .NET 10 + +# ALM +- CHANGED Target frameworks by adding .NET 11 (net11.0) and removing .NET 9 (net9.0), so the package now targets .NET 11 and .NET 10 + +Version: 1.3.5 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +Version: 1.3.4 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +Version: 1.3.3 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +Version: 1.3.2 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +Version: 1.3.1 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 1.3.0 Availability: .NET 10 and .NET 9 diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt index 42d7785..01db84b 100644 --- a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt @@ -1,33 +1,39 @@ -Version: 1.3.5 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -Version: 1.3.4 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -Version: 1.3.3 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -Version: 1.3.2 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - -Version: 1.3.1 -Availability: .NET 10 and .NET 9 - -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) - +Version: 2.0.0 +Availability: .NET 11 and .NET 10 + +# ALM +- CHANGED Target frameworks by adding .NET 11 (net11.0) and removing .NET 9 (net9.0), so the package now targets .NET 11 and .NET 10 + +Version: 1.3.5 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +Version: 1.3.4 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +Version: 1.3.3 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +Version: 1.3.2 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + +Version: 1.3.1 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 1.3.0 Availability: .NET 10 and .NET 9 diff --git a/AGENTS.md b/AGENTS.md index 0641fc1..f7a0c57 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This document provides guidance for AI agents working in this repository. ## Project Overview -Codebelt.Extensions.BenchmarkDotNet is a .NET library that provides a uniform, opinionated, and extensible way of working with BenchmarkDotNet. The solution targets .NET 10.0 and .NET 9.0. +Codebelt.Extensions.BenchmarkDotNet is a .NET library that provides a uniform, opinionated, and extensible way of working with BenchmarkDotNet. The solution targets .NET 11.0 and .NET 10.0. ## Coding Standards diff --git a/Directory.Build.props b/Directory.Build.props index 0283897..ea668f7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ - net10.0;net9.0 + net11.0;net10.0 Copyright © Geekle 2025-2026. All rights reserved. gimlichael Geekle @@ -56,7 +56,7 @@ - net10.0;net9.0 + net11.0;net10.0 false Exe false @@ -94,7 +94,7 @@ - net10.0;net9.0 + net11.0;net10.0 false false false diff --git a/Directory.Packages.props b/Directory.Packages.props index 9d589af..43fa915 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,10 +3,10 @@ true - + - + @@ -16,4 +16,4 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 52585e3..47111f7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ An open-source project (MIT license) that targets and complements the [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) performance library. It provides a uniform and convenient way of doing benchmarking for all project types in .NET. -Your versatile BenchmarkDotNet companion for modern development with `.NET 9` and `.NET 10`. +Your versatile BenchmarkDotNet companion for modern development with `.NET 10` and `.NET 11`. It is, by heart, free, flexible and built to extend and boost your agile codebelt. @@ -60,6 +60,7 @@ Benchmarks are executed through a console-hosted Generic Host, allowing full par using BenchmarkDotNet.Configs; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; +using Codebelt.Extensions.BenchmarkDotNet; using Codebelt.Extensions.BenchmarkDotNet.Console; public class Program @@ -72,8 +73,8 @@ public class Program o.ConfigureBenchmarkDotNet(c => { var slimJob = BenchmarkWorkspaceOptions.Slim; - return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) - .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)) + .AddJob(slimJob.WithRuntime(CoreRuntime.CreateForNewVersion("net11.0", ".NET 11.0"))); }); }); } diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs index 4942782..00d6747 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs @@ -1,22 +1,22 @@ -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -/// -/// Represents the command-line context for a benchmark run. -/// -public class BenchmarkContext -{ - /// - /// Initializes a new instance of the class. - /// - /// The command-line arguments passed to the . - public BenchmarkContext(string[] args) - { - Args = args ?? []; - } - - /// - /// Gets the command-line arguments passed to the . - /// - /// The command-line arguments. - public string[] Args { get; } -} +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +/// +/// Represents the command-line context for a benchmark run. +/// +public class BenchmarkContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The command-line arguments passed to the . + public BenchmarkContext(string[] args) + { + Args = args ?? []; + } + + /// + /// Gets the command-line arguments passed to the . + /// + /// The command-line arguments. + public string[] Args { get; } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs index f10d006..4292ee1 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs @@ -1,260 +1,259 @@ -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Filters; -using BenchmarkDotNet.Running; -using Codebelt.Bootstrapper.Console; -using Cuemon; -using Cuemon.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console -{ - /// - /// Entry point helper for hosting and running benchmarks using BenchmarkDotNet. - /// - /// - public class BenchmarkProgram : MinimalConsoleProgram - { - static BenchmarkProgram() - { - var isDebugBuild = Decorator.Enclose(Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).IsDebugBuild(); - BuildConfiguration = isDebugBuild ? "Debug" : "Release"; - IsDebugBuild = isDebugBuild; - } - - /// - /// Gets the build configuration of the entry assembly. - /// - /// The value is either Debug or Release. - public static string BuildConfiguration { get; } - - /// - /// Gets a value indicating whether the entry assembly was built in Debug configuration. - /// - /// true if the entry assembly was compiled with debugging information; otherwise, false. - public static bool IsDebugBuild { get; } - - /// - /// Runs benchmarks using the default implementation. - /// - /// The command-line arguments passed to the application. - /// The which may be configured. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. - /// - public static void Run(string[] args, Action setup = null) - { - Run(args, null, setup); - } - - /// - /// Runs benchmarks using the default implementation. - /// - /// The command-line arguments passed to the application. - /// The delegate that will be invoked to configure additional services in the . - /// The which may be configured. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. - /// - public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null) - { - Run(args, serviceConfigurator, setup); - } - - /// - /// Runs benchmarks using a custom implementation of . - /// - /// The type of the workspace that implements . - /// The command-line arguments passed to the application. - /// The which may be configured. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. - /// - public static void Run(string[] args, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace - { - Run(args, null, setup); - } - - /// - /// Runs benchmarks using a custom implementation of . - /// - /// The type of the workspace that implements . - /// The command-line arguments passed to the application. - /// The delegate that will be invoked to configure additional services in the . - /// The which may be configured. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. - /// - public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace - { - using var host = BuildHost(args, serviceConfigurator, setup); - host.Run(); - } +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Filters; +using BenchmarkDotNet.Running; +using Codebelt.Bootstrapper.Console; +using Cuemon; +using Cuemon.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; - /// - /// Runs benchmarks asynchronously using the default implementation. - /// - /// The command-line arguments passed to the application. - /// The which may be configured. - /// A task that represents the asynchronous benchmark host operation. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. - /// - public static Task RunAsync(string[] args, Action setup = null) - { - return RunAsync(args, null, setup); - } +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +/// +/// Entry point helper for hosting and running benchmarks using BenchmarkDotNet. +/// +/// +public class BenchmarkProgram : MinimalConsoleProgram +{ + static BenchmarkProgram() + { + var isDebugBuild = Decorator.Enclose(Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).IsDebugBuild(); + BuildConfiguration = isDebugBuild ? "Debug" : "Release"; + IsDebugBuild = isDebugBuild; + } + + /// + /// Gets the build configuration of the entry assembly. + /// + /// The value is either Debug or Release. + public static string BuildConfiguration { get; } + + /// + /// Gets a value indicating whether the entry assembly was built in Debug configuration. + /// + /// true if the entry assembly was compiled with debugging information; otherwise, false. + public static bool IsDebugBuild { get; } + + /// + /// Runs benchmarks using the default implementation. + /// + /// The command-line arguments passed to the application. + /// The which may be configured. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. + /// + public static void Run(string[] args, Action setup = null) + { + Run(args, null, setup); + } + + /// + /// Runs benchmarks using the default implementation. + /// + /// The command-line arguments passed to the application. + /// The delegate that will be invoked to configure additional services in the . + /// The which may be configured. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. + /// + public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null) + { + Run(args, serviceConfigurator, setup); + } + + /// + /// Runs benchmarks using a custom implementation of . + /// + /// The type of the workspace that implements . + /// The command-line arguments passed to the application. + /// The which may be configured. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. + /// + public static void Run(string[] args, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + Run(args, null, setup); + } + + /// + /// Runs benchmarks using a custom implementation of . + /// + /// The type of the workspace that implements . + /// The command-line arguments passed to the application. + /// The delegate that will be invoked to configure additional services in the . + /// The which may be configured. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. + /// + public static void Run(string[] args, Action serviceConfigurator = null, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + using var host = BuildHost(args, serviceConfigurator, setup); + host.Run(); + } + + /// + /// Runs benchmarks asynchronously using the default implementation. + /// + /// The command-line arguments passed to the application. + /// The which may be configured. + /// A task that represents the asynchronous benchmark host operation. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. + /// + public static Task RunAsync(string[] args, Action setup = null) + { + return RunAsync(args, null, setup); + } + + /// + /// Runs benchmarks asynchronously using the default implementation. + /// + /// The command-line arguments passed to the application. + /// The delegate that will be invoked to configure additional services in the . + /// The which may be configured. + /// A task that represents the asynchronous benchmark host operation. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. + /// + public static Task RunAsync(string[] args, Action serviceConfigurator = null, Action setup = null) + { + return RunAsync(args, serviceConfigurator, setup); + } + + /// + /// Runs benchmarks asynchronously using a custom implementation of . + /// + /// The type of the workspace that implements . + /// The command-line arguments passed to the application. + /// The which may be configured. + /// A task that represents the asynchronous benchmark host operation. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. + /// + public static Task RunAsync(string[] args, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + return RunAsync(args, null, setup); + } + + /// + /// Runs benchmarks asynchronously using a custom implementation of . + /// + /// The type of the workspace that implements . + /// The command-line arguments passed to the application. + /// The delegate that will be invoked to configure additional services in the . + /// The which may be configured. + /// A task that represents the asynchronous benchmark host operation. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. + /// + public static async Task RunAsync(string[] args, Action serviceConfigurator = null, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + using var host = BuildHost(args, serviceConfigurator, setup); + await host.RunAsync().ConfigureAwait(false); + } + + /// + /// Runs the actual benchmarks as envisioned by BenchmarkDotNet. + /// + /// The service provider. + /// The cancellation token. + /// A completed task when benchmark execution has finished. + /// + /// When arguments are provided, they are forwarded to for selective execution. + /// After execution completes, artifact post-processing is performed. + /// + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + var options = serviceProvider.GetRequiredService(); + var workspace = serviceProvider.GetRequiredService(); + var assemblies = workspace.LoadBenchmarkAssemblies(); + var context = serviceProvider.GetRequiredService(); - /// - /// Runs benchmarks asynchronously using the default implementation. - /// - /// The command-line arguments passed to the application. - /// The delegate that will be invoked to configure additional services in the . - /// The which may be configured. - /// A task that represents the asynchronous benchmark host operation. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. - /// - public static Task RunAsync(string[] args, Action serviceConfigurator = null, Action setup = null) + if (options.SkipBenchmarksWithReports) { ConfigureBenchmarkDotNetFiltersForExistingReports(options, assemblies); } + + try { - return RunAsync(args, serviceConfigurator, setup); + ExecuteBenchmarks(assemblies, context, options); } - - /// - /// Runs benchmarks asynchronously using a custom implementation of . - /// - /// The type of the workspace that implements . - /// The command-line arguments passed to the application. - /// The which may be configured. - /// A task that represents the asynchronous benchmark host operation. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. - /// - public static Task RunAsync(string[] args, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + finally { - return RunAsync(args, null, setup); + workspace.PostProcessArtifacts(); } - /// - /// Runs benchmarks asynchronously using a custom implementation of . - /// - /// The type of the workspace that implements . - /// The command-line arguments passed to the application. - /// The delegate that will be invoked to configure additional services in the . - /// The which may be configured. - /// A task that represents the asynchronous benchmark host operation. - /// - /// This method configures the host builder with the necessary services, builds the host, and runs it asynchronously to execute benchmarks. - /// - public static async Task RunAsync(string[] args, Action serviceConfigurator = null, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + return Task.CompletedTask; + } + + private static IHost BuildHost(string[] args, Action serviceConfigurator, Action setup) where TWorkspace : class, IBenchmarkWorkspace + { + var builder = CreateHostBuilder(args); + + builder.Services.Configure(o => o.SuppressStatusMessages = !IsDebugBuild); + builder.Services.AddSingleton(new BenchmarkContext(args)); + builder.Services.AddBenchmarkWorkspace(setup); + serviceConfigurator?.Invoke(builder.Services); + + return builder.Build(); + } + + private static void ConfigureBenchmarkDotNetFiltersForExistingReports(BenchmarkWorkspaceOptions options, Assembly[] assemblies) + { + var benchmarkTypes = assemblies + .SelectMany(a => a.GetTypes().Where(t => t.Name.EndsWith("Benchmark", StringComparison.Ordinal))) + .ToList(); + + options.ConfigureBenchmarkDotNet(c => ApplyReportFilters(c, options, benchmarkTypes)); + } + + private static IConfig ApplyReportFilters(IConfig config, BenchmarkWorkspaceOptions options, List benchmarkTypes) + { + var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); + if (!Directory.Exists(tuningPath)) { return config; } + + foreach (var report in Directory.EnumerateFiles(tuningPath)) { - using var host = BuildHost(args, serviceConfigurator, setup); - await host.RunAsync().ConfigureAwait(false); + var matchingType = FindMatchingBenchmarkType(report, benchmarkTypes); + if (matchingType != null) + { + config = config.AddFilter(new SimpleFilter(bc => bc.Descriptor.Type != matchingType)); + } } - /// - /// Runs the actual benchmarks as envisioned by BenchmarkDotNet. - /// - /// The service provider. - /// The cancellation token. - /// A completed task when benchmark execution has finished. - /// - /// When arguments are provided, they are forwarded to for selective execution. - /// After execution completes, artifact post-processing is performed. - /// - public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) - { - var options = serviceProvider.GetRequiredService(); - var workspace = serviceProvider.GetRequiredService(); - var assemblies = workspace.LoadBenchmarkAssemblies(); - var context = serviceProvider.GetRequiredService(); - - if (options.SkipBenchmarksWithReports) { ConfigureBenchmarkDotNetFiltersForExistingReports(options, assemblies); } - - try - { - ExecuteBenchmarks(assemblies, context, options); - } - finally - { - workspace.PostProcessArtifacts(); - } - - return Task.CompletedTask; - } + return config; + } - private static IHost BuildHost(string[] args, Action serviceConfigurator, Action setup) where TWorkspace : class, IBenchmarkWorkspace - { - var builder = CreateHostBuilder(args); + private static Type FindMatchingBenchmarkType(string reportPath, List benchmarkTypes) + { + var filename = Path.GetFileNameWithoutExtension(reportPath); + var potentialTypeFullName = filename.Split('-').FirstOrDefault(); + if (string.IsNullOrWhiteSpace(potentialTypeFullName)) { return null; } - builder.Services.Configure(o => o.SuppressStatusMessages = !IsDebugBuild); - builder.Services.AddSingleton(new BenchmarkContext(args)); - builder.Services.AddBenchmarkWorkspace(setup); - serviceConfigurator?.Invoke(builder.Services); + var potentialTypeName = potentialTypeFullName.Split('.').LastOrDefault(); + if (string.IsNullOrWhiteSpace(potentialTypeName)) { return null; } - return builder.Build(); - } + return benchmarkTypes.FirstOrDefault(t => t.Name.Equals(potentialTypeName, StringComparison.OrdinalIgnoreCase)); + } - private static void ConfigureBenchmarkDotNetFiltersForExistingReports(BenchmarkWorkspaceOptions options, Assembly[] assemblies) + private static void ExecuteBenchmarks(Assembly[] assemblies, BenchmarkContext context, BenchmarkWorkspaceOptions options) + { + if (context.Args.Length == 0) { - var benchmarkTypes = assemblies - .SelectMany(a => a.GetTypes().Where(t => t.Name.EndsWith("Benchmark", StringComparison.Ordinal))) - .ToList(); - - options.ConfigureBenchmarkDotNet(c => ApplyReportFilters(c, options, benchmarkTypes)); - } - - private static IConfig ApplyReportFilters(IConfig config, BenchmarkWorkspaceOptions options, List benchmarkTypes) - { - var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); - if (!Directory.Exists(tuningPath)) { return config; } - - foreach (var report in Directory.EnumerateFiles(tuningPath)) - { - var matchingType = FindMatchingBenchmarkType(report, benchmarkTypes); - if (matchingType != null) - { - config = config.AddFilter(new SimpleFilter(bc => bc.Descriptor.Type != matchingType)); - } - } - - return config; - } - - private static Type FindMatchingBenchmarkType(string reportPath, List benchmarkTypes) - { - var filename = Path.GetFileNameWithoutExtension(reportPath); - var potentialTypeFullName = filename.Split('-').FirstOrDefault(); - if (string.IsNullOrWhiteSpace(potentialTypeFullName)) { return null; } - - var potentialTypeName = potentialTypeFullName.Split('.').LastOrDefault(); - if (string.IsNullOrWhiteSpace(potentialTypeName)) { return null; } - - return benchmarkTypes.FirstOrDefault(t => t.Name.Equals(potentialTypeName, StringComparison.OrdinalIgnoreCase)); - } - - private static void ExecuteBenchmarks(Assembly[] assemblies, BenchmarkContext context, BenchmarkWorkspaceOptions options) - { - if (context.Args.Length == 0) - { - foreach (var assembly in assemblies) - { - BenchmarkRunner.Run(assembly, options.Configuration); - } - } - else - { - BenchmarkSwitcher - .FromAssemblies(assemblies) - .Run(context.Args, options.Configuration); - } - } - } -} + foreach (var assembly in assemblies) + { + BenchmarkRunner.Run(assembly, options.Configuration); + } + } + else + { + BenchmarkSwitcher + .FromAssemblies(assemblies) + .Run(context.Args, options.Configuration); + } + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs index ca6932d..1f2e5d9 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs @@ -1,225 +1,225 @@ -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Toolchains.InProcess.Emit; -using Cuemon; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -/// -/// Provides a default implementation of for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet. -/// -public sealed class BenchmarkWorkspace : IBenchmarkWorkspace -{ - private readonly BenchmarkWorkspaceOptions _options; - private static bool _assemblyResolverRegistered; - private static readonly Lock AssemblyResolverLock = new(); - private static Dictionary _assemblyLookup = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Gets the path to the BenchmarkDotNet results directory - /// - /// The which configures repository paths, build modes and BenchmarkDotNet configuration. - /// A string containing the full path to the 'results' directory within the artifacts path. - /// - /// This path is constructed from the configured and the "results" subdirectory. - /// - public static string GetReportsResultsPath(BenchmarkWorkspaceOptions options) - { - Validator.ThrowIfInvalidOptions(options); - return Path.Combine(options.Configuration.ArtifactsPath, "results"); - } - - /// - /// Gets the path to the tuning folder where final benchmark reports are stored. - /// - /// The which configures repository paths, build modes and BenchmarkDotNet configuration. - /// A string representing the full path to the tuning reports directory. - /// - /// This path is constructed from the configured and the . - /// - public static string GetReportsTuningPath(BenchmarkWorkspaceOptions options) - { - Validator.ThrowIfInvalidOptions(options); - return Path.Combine(options.Configuration.ArtifactsPath, options.RepositoryTuningFolder); - } - - /// - /// Initializes a new instance of the class with the specified options. - /// - /// The which configures repository paths, build modes and BenchmarkDotNet configuration. - /// - /// cannot be null. - /// - /// - /// are not in a valid state. - /// - public BenchmarkWorkspace(BenchmarkWorkspaceOptions options) - { - Validator.ThrowIfInvalidOptions(options); - if (options.AllowDebugBuild && options.Configuration is ManualConfig mc) - { - mc.Options |= ConfigOptions.DisableOptimizationsValidator; - options.Configuration = mc.AddJob(Job.Default.WithToolchain(new InProcessEmitToolchain(TimeSpan.FromHours(1), true))); - } - _options = options; - } - - /// - /// Loads benchmark assemblies discovered recursively in the tuning folder. - /// - /// An array of instances representing the loaded benchmark assemblies. - /// - /// Assemblies are selected by matching the configured , - /// the build configuration (Debug/Release based on ), - /// and the target framework moniker (). - /// - /// - /// Thrown when no matching assemblies could be loaded from the tuning folder. Ensure the tuning folder contains - /// built benchmark assemblies for the configured build configuration and TFM. - /// - public Assembly[] LoadBenchmarkAssemblies() - { - var useDebugBuild = _options.AllowDebugBuild; - return LoadAssemblies( - _options.RepositoryPath, - _options.TargetFrameworkMoniker, - _options.BenchmarkProjectSuffix, - _options.RepositoryTuningFolder, - useDebugBuild).ToArray(); - } - - /// - /// Performs post-processing of artifacts produced by BenchmarkDotNet. - /// - /// - /// This method moves files found in the BenchmarkDotNet artifacts "results" directory into the tuning folder under the configured artifacts path and then deletes the now-empty "results" directory. - /// - public void PostProcessArtifacts() - { - CleanupResults(GetReportsResultsPath(_options), GetReportsTuningPath(_options)); - } - - private static IEnumerable LoadAssemblies(string repositoryPath, string targetFrameworkMoniker, string benchmarkProjectSuffix, string repositoryTuningFolder, bool useDebugBuild) - { - var tuningDir = Path.Combine(repositoryPath, repositoryTuningFolder); - Directory.CreateDirectory(tuningDir); - - UpdateAssemblyLookup(tuningDir); - EnsureAssemblyResolverRegistered(); - - var debugOrRelease = useDebugBuild ? "Debug" : "Release"; - var buildSegment = Path.Combine("bin", debugOrRelease, targetFrameworkMoniker); - - var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies(); - var assemblies = new List(); - - var candidatePaths = Directory - .EnumerateFiles(tuningDir, $"*.{benchmarkProjectSuffix}.dll", SearchOption.AllDirectories) - .Where(path => path.Contains(buildSegment, StringComparison.OrdinalIgnoreCase)); - - foreach (var path in candidatePaths) - { - try - { - var candidateName = AssemblyName.GetAssemblyName(path); - var existing = alreadyLoaded.FirstOrDefault(a => - AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName)); - - if (existing != null) - { - assemblies.Add(existing); - continue; - } - - if (assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName))) - { - continue; - } - - assemblies.Add(Assembly.LoadFrom(path)); - } - catch - { - // swallow and continue - } - } - - if (assemblies.Count == 0) - { - throw new InvalidOperationException($"No assemblies were loaded. Ensure that '{tuningDir}' contains assemblies for '{debugOrRelease}' build and '{targetFrameworkMoniker}' moniker."); - } - - return assemblies; - } - - private static void UpdateAssemblyLookup(string tuningDir) - { - var allDlls = Directory.EnumerateFiles(tuningDir, "*.dll", SearchOption.AllDirectories); - var map = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var path in allDlls) - { - var simpleName = Path.GetFileNameWithoutExtension(path); - if (string.IsNullOrEmpty(simpleName)) - { - continue; - } - map[simpleName] = path; - } - _assemblyLookup = map; - } - - private static void EnsureAssemblyResolverRegistered() - { - lock (AssemblyResolverLock) - { - if (_assemblyResolverRegistered) { return; } - - AppDomain.CurrentDomain.AssemblyResolve += (_, args) => - { - try - { - var requestedName = new AssemblyName(args.Name).Name; - if (string.IsNullOrEmpty(requestedName)) { return null; } - - if (!_assemblyLookup.TryGetValue(requestedName, out var path) || !File.Exists(path)) - { - return null; - } - - var asmName = AssemblyName.GetAssemblyName(path); - var already = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), asmName)); - if (already != null) { return already; } - - return Assembly.LoadFrom(path); - } - catch - { - return null; - } - }; - - _assemblyResolverRegistered = true; - } - } - - private static void CleanupResults(string reportsResultsPath, string reportsTuningPath) - { - if (!Directory.Exists(reportsResultsPath)) { return; } - - Directory.CreateDirectory(reportsTuningPath); - - foreach (var file in Directory.GetFiles(reportsResultsPath).Where(f => !Path.GetFileNameWithoutExtension(f).StartsWith("BenchmarkRun", StringComparison.Ordinal))) - { - var targetFile = Path.Combine(reportsTuningPath, Path.GetFileName(file)); - File.Move(file, targetFile, true); - } - - Directory.Delete(reportsResultsPath, recursive: true); - } -} +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using Cuemon; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Provides a default implementation of for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet. +/// +public sealed class BenchmarkWorkspace : IBenchmarkWorkspace +{ + private readonly BenchmarkWorkspaceOptions _options; + private static bool _assemblyResolverRegistered; + private static readonly Lock AssemblyResolverLock = new(); + private static Dictionary _assemblyLookup = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets the path to the BenchmarkDotNet results directory + /// + /// The which configures repository paths, build modes and BenchmarkDotNet configuration. + /// A string containing the full path to the 'results' directory within the artifacts path. + /// + /// This path is constructed from the configured and the "results" subdirectory. + /// + public static string GetReportsResultsPath(BenchmarkWorkspaceOptions options) + { + Validator.ThrowIfInvalidOptions(options); + return Path.Combine(options.Configuration.ArtifactsPath, "results"); + } + + /// + /// Gets the path to the tuning folder where final benchmark reports are stored. + /// + /// The which configures repository paths, build modes and BenchmarkDotNet configuration. + /// A string representing the full path to the tuning reports directory. + /// + /// This path is constructed from the configured and the . + /// + public static string GetReportsTuningPath(BenchmarkWorkspaceOptions options) + { + Validator.ThrowIfInvalidOptions(options); + return Path.Combine(options.Configuration.ArtifactsPath, options.RepositoryTuningFolder); + } + + /// + /// Initializes a new instance of the class with the specified options. + /// + /// The which configures repository paths, build modes and BenchmarkDotNet configuration. + /// + /// cannot be null. + /// + /// + /// are not in a valid state. + /// + public BenchmarkWorkspace(BenchmarkWorkspaceOptions options) + { + Validator.ThrowIfInvalidOptions(options); + if (options.AllowDebugBuild && options.Configuration is ManualConfig mc) + { + mc.Options |= ConfigOptions.DisableOptimizationsValidator; + options.Configuration = mc.AddJob(Job.Default.WithToolchain(new InProcessEmitToolchain(TimeSpan.FromHours(1), true))); + } + _options = options; + } + + /// + /// Loads benchmark assemblies discovered recursively in the tuning folder. + /// + /// An array of instances representing the loaded benchmark assemblies. + /// + /// Assemblies are selected by matching the configured , + /// the build configuration (Debug/Release based on ), + /// and the target framework moniker (). + /// + /// + /// Thrown when no matching assemblies could be loaded from the tuning folder. Ensure the tuning folder contains + /// built benchmark assemblies for the configured build configuration and TFM. + /// + public Assembly[] LoadBenchmarkAssemblies() + { + var useDebugBuild = _options.AllowDebugBuild; + return LoadAssemblies( + _options.RepositoryPath, + _options.TargetFrameworkMoniker, + _options.BenchmarkProjectSuffix, + _options.RepositoryTuningFolder, + useDebugBuild).ToArray(); + } + + /// + /// Performs post-processing of artifacts produced by BenchmarkDotNet. + /// + /// + /// This method moves files found in the BenchmarkDotNet artifacts "results" directory into the tuning folder under the configured artifacts path and then deletes the now-empty "results" directory. + /// + public void PostProcessArtifacts() + { + CleanupResults(GetReportsResultsPath(_options), GetReportsTuningPath(_options)); + } + + private static IEnumerable LoadAssemblies(string repositoryPath, string targetFrameworkMoniker, string benchmarkProjectSuffix, string repositoryTuningFolder, bool useDebugBuild) + { + var tuningDir = Path.Combine(repositoryPath, repositoryTuningFolder); + Directory.CreateDirectory(tuningDir); + + UpdateAssemblyLookup(tuningDir); + EnsureAssemblyResolverRegistered(); + + var debugOrRelease = useDebugBuild ? "Debug" : "Release"; + var buildSegment = Path.Combine("bin", debugOrRelease, targetFrameworkMoniker); + + var alreadyLoaded = AppDomain.CurrentDomain.GetAssemblies(); + var assemblies = new List(); + + var candidatePaths = Directory + .EnumerateFiles(tuningDir, $"*.{benchmarkProjectSuffix}.dll", SearchOption.AllDirectories) + .Where(path => path.Contains(buildSegment, StringComparison.OrdinalIgnoreCase)); + + foreach (var path in candidatePaths) + { + try + { + var candidateName = AssemblyName.GetAssemblyName(path); + var existing = alreadyLoaded.FirstOrDefault(a => + AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName)); + + if (existing != null) + { + assemblies.Add(existing); + continue; + } + + if (assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName))) + { + continue; + } + + assemblies.Add(Assembly.LoadFrom(path)); + } + catch + { + // swallow and continue + } + } + + if (assemblies.Count == 0) + { + throw new InvalidOperationException($"No assemblies were loaded. Ensure that '{tuningDir}' contains assemblies for '{debugOrRelease}' build and '{targetFrameworkMoniker}' moniker."); + } + + return assemblies; + } + + private static void UpdateAssemblyLookup(string tuningDir) + { + var allDlls = Directory.EnumerateFiles(tuningDir, "*.dll", SearchOption.AllDirectories); + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var path in allDlls) + { + var simpleName = Path.GetFileNameWithoutExtension(path); + if (string.IsNullOrEmpty(simpleName)) + { + continue; + } + map[simpleName] = path; + } + _assemblyLookup = map; + } + + private static void EnsureAssemblyResolverRegistered() + { + lock (AssemblyResolverLock) + { + if (_assemblyResolverRegistered) { return; } + + AppDomain.CurrentDomain.AssemblyResolve += (_, args) => + { + try + { + var requestedName = new AssemblyName(args.Name).Name; + if (string.IsNullOrEmpty(requestedName)) { return null; } + + if (!_assemblyLookup.TryGetValue(requestedName, out var path) || !File.Exists(path)) + { + return null; + } + + var asmName = AssemblyName.GetAssemblyName(path); + var already = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), asmName)); + if (already != null) { return already; } + + return Assembly.LoadFrom(path); + } + catch + { + return null; + } + }; + + _assemblyResolverRegistered = true; + } + } + + private static void CleanupResults(string reportsResultsPath, string reportsTuningPath) + { + if (!Directory.Exists(reportsResultsPath)) { return; } + + Directory.CreateDirectory(reportsTuningPath); + + foreach (var file in Directory.GetFiles(reportsResultsPath).Where(f => !Path.GetFileNameWithoutExtension(f).StartsWith("BenchmarkRun", StringComparison.Ordinal))) + { + var targetFile = Path.Combine(reportsTuningPath, Path.GetFileName(file)); + File.Move(file, targetFile, true); + } + + Directory.Delete(reportsResultsPath, recursive: true); + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs index 8dfec54..1bf80f9 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs @@ -1,343 +1,343 @@ -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Exporters; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Loggers; -using BenchmarkDotNet.Reports; -using Cuemon; -using Cuemon.Configuration; -using Perfolizer.Horology; -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.Versioning; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -/// -/// Configuration options for . -/// -/// -/// The following table shows the initial property values for an instance of . -/// -/// -/// Property -/// Initial Value -/// -/// -/// -/// Resolved runtime by filesystem, e.g. C:\Repos\MyBenchmarkRepo. -/// -/// -/// -/// BenchmarkDotNet configured to use recommended settings as outlined in: https://github.com/dotnet/performance/blob/main/src/harness/BenchmarkDotNet.Extensions/RecommendedConfig.cs. -/// -/// -/// -/// Resolved runtime by reflection, e.g. net10.0. -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// false -/// -/// -/// -/// false -/// -/// -/// -public class BenchmarkWorkspaceOptions : IValidatableParameterObject, IPostConfigurableParameterObject -{ - /// - /// The default folder name where benchmark reports are written relative to the repository path. - /// - public const string DefaultRepositoryReportsFolder = "reports"; - - /// - /// The default folder name used for tuning artifacts relative to the repository path. - /// - public const string DefaultRepositoryTuningFolder = "tuning"; - - /// - /// The default suffix used to identify benchmark projects. - /// - public const string DefaultBenchmarkProjectSuffix = "Benchmarks"; - - /// - /// A tuned preset that serves as a fast, reliable baseline for most benchmarks, balancing measurement accuracy with developer efficiency. - /// - /// - /// A configured with reduced warmup, shortened iteration duration, controlled iteration counts, and without system power plan enforcement. - /// - /// - /// Based on the recommended configuration used in the .NET Performance repository: https://github.com/dotnet/performance/blob/main/src/harness/BenchmarkDotNet.Extensions/RecommendedConfig.cs - /// - public static readonly Job Slim = GetDefaultConfiguredJob(); - - private static readonly string DefaultRepositoryPath = GetDefaultRepositoryPath(); - private static readonly string DefaultTargetFrameworkMoniker = ResolveCurrentTfm(); - - /// - /// Initializes a new instance of the class with sensible defaults. - /// - public BenchmarkWorkspaceOptions() - { - Configuration = GetDefaultConfiguration(); - RepositoryPath = DefaultRepositoryPath; - TargetFrameworkMoniker = DefaultTargetFrameworkMoniker; - RepositoryTuningFolder = DefaultRepositoryTuningFolder; - RepositoryReportsFolder = DefaultRepositoryReportsFolder; - BenchmarkProjectSuffix = DefaultBenchmarkProjectSuffix; - } - - /// - /// Gets or sets the root path of the repository where benchmark projects and reports live. - /// - /// The repository root path. - public string RepositoryPath { get; set; } - - /// - /// Gets or sets the instance used by BenchmarkDotNet. - /// - /// The BenchmarkDotNet configuration. - public IConfig Configuration { get; set; } - - /// - /// Gets or sets the target framework moniker (TFM) string to be used for benchmark discovery and reporting. - /// - /// The target framework moniker to run benchmarks for, e.g. net10.0. - public string TargetFrameworkMoniker { get; set; } - - /// - /// Gets or sets the folder name under used to store tuning artifacts. - /// - /// The tuning folder name. Defaults to . - public string RepositoryTuningFolder { get; set; } - - /// - /// Gets or sets the folder name under used to store generated reports. - /// - /// The reports folder name. Defaults to . - public string RepositoryReportsFolder { get; set; } - - /// - /// Gets or sets the project name suffix used to identify benchmark projects. - /// - /// - /// The benchmark project suffix. Defaults to . - /// - public string BenchmarkProjectSuffix { get; set; } - - /// - /// Gets or sets a value indicating whether the benchmarks should be allowed to run using a Debug build. - /// - /// - /// true to allow Debug builds for benchmarks; otherwise, false. Default is false. - /// - public bool AllowDebugBuild { get; set; } - - /// - /// Gets or sets a value indicating whether benchmarks that already have generated reports should be skipped during execution. - /// - /// - /// true to skip benchmarks with existing reports; otherwise, false. Default is false. - /// - public bool SkipBenchmarksWithReports { get; set; } - - /// - /// Finalizes the configured options before use. - /// - /// This method updates the to set the BenchmarkDotNet artifacts path to the combination of and . - public void PostConfigureOptions() - { - if (string.IsNullOrWhiteSpace(Configuration.ArtifactsPath)) - { - var artifactsPath = Path.Combine(RepositoryPath, RepositoryReportsFolder); - - if (Configuration is ManualConfig manual) - { - manual.ArtifactsPath = artifactsPath; - } - else - { - Configuration = Configuration.WithArtifactsPath(artifactsPath); - } - } - } - - /// - /// Determines whether the public read-write properties of this instance are in a valid state. - /// - /// - /// cannot be . - /// -or- - /// cannot be , empty or consist only of white-space characters. - /// -or- - /// cannot be , empty or consist only of white-space characters. - /// -or- - /// cannot be , empty or consist only of white-space characters. - /// -or- - /// cannot be , empty or consist only of white-space characters. - /// -or- - /// cannot be , empty or consist only of white-space characters. - /// - public void ValidateOptions() - { - Validator.ThrowIfInvalidState(Configuration == null); - Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryPath)); - Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryTuningFolder)); - Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryReportsFolder)); - Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(TargetFrameworkMoniker)); - Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(BenchmarkProjectSuffix)); - } - - private static string GetDefaultRepositoryPath() - { - var dir = AppContext.BaseDirectory; - while (!string.IsNullOrWhiteSpace(dir)) - { - if (Directory.Exists(Path.Combine(dir, ".git"))) - { - return dir; - } - dir = Path.GetDirectoryName(dir); - } - return Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."); - } - - private static ManualConfig GetDefaultConfiguration() - { - var config = ManualConfig.CreateEmpty() - .WithBuildTimeout(TimeSpan.FromMinutes(15)) // for slow machines - .AddLogger(ConsoleLogger.Default) // log output to console - .AddValidator(DefaultConfig.Instance.GetValidators().ToArray()) // copy default validators - .AddAnalyser(DefaultConfig.Instance.GetAnalysers().ToArray()) // copy default analysers - .AddExporter(MarkdownExporter.GitHub) // export to GitHub markdown - .AddColumnProvider(DefaultColumnProviders.Instance) // display default columns (method name, args etc) - .AddJob(Slim.AsDefault()) // tell BDN that this are our default settings - .AddDiagnoser(MemoryDiagnoser.Default) // MemoryDiagnoser is enabled by default - .AddColumn(StatisticColumn.Median, StatisticColumn.Min, StatisticColumn.Max) - .WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(36)); // the default is 20 and trims too aggressively some benchmark results - config.Options = ConfigOptions.DisableLogFile; - return config; - } - - private static Job GetDefaultConfiguredJob() - { - return Job.Default - .WithWarmupCount(1) // 1 warmup is enough for our purpose - .WithIterationTime(TimeInterval.FromMilliseconds(250)) // the default is 0.5s per iteration, which is slightly too much for us - .WithMinIterationCount(15) - .WithMaxIterationCount(20) // we don't want to run more than 20 iterations - .DontEnforcePowerPlan(); // make sure BDN does not try to enforce High Performance power plan on Windows - } - +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Reports; +using Cuemon; +using Cuemon.Configuration; +using Perfolizer.Horology; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Configuration options for . +/// +/// +/// The following table shows the initial property values for an instance of . +/// +/// +/// Property +/// Initial Value +/// +/// +/// +/// Resolved runtime by filesystem, e.g. C:\Repos\MyBenchmarkRepo. +/// +/// +/// +/// BenchmarkDotNet configured to use recommended settings as outlined in: https://github.com/dotnet/performance/blob/main/src/harness/BenchmarkDotNet.Extensions/RecommendedConfig.cs. +/// +/// +/// +/// Resolved runtime by reflection, e.g. net10.0. +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// false +/// +/// +/// +/// false +/// +/// +/// +public class BenchmarkWorkspaceOptions : IValidatableParameterObject, IPostConfigurableParameterObject +{ + /// + /// The default folder name where benchmark reports are written relative to the repository path. + /// + public const string DefaultRepositoryReportsFolder = "reports"; + + /// + /// The default folder name used for tuning artifacts relative to the repository path. + /// + public const string DefaultRepositoryTuningFolder = "tuning"; + + /// + /// The default suffix used to identify benchmark projects. + /// + public const string DefaultBenchmarkProjectSuffix = "Benchmarks"; + + /// + /// A tuned preset that serves as a fast, reliable baseline for most benchmarks, balancing measurement accuracy with developer efficiency. + /// + /// + /// A configured with reduced warmup, shortened iteration duration, controlled iteration counts, and without system power plan enforcement. + /// + /// + /// Based on the recommended configuration used in the .NET Performance repository: https://github.com/dotnet/performance/blob/main/src/harness/BenchmarkDotNet.Extensions/RecommendedConfig.cs + /// + public static readonly Job Slim = GetDefaultConfiguredJob(); + + private static readonly string DefaultRepositoryPath = GetDefaultRepositoryPath(); + private static readonly string DefaultTargetFrameworkMoniker = ResolveCurrentTfm(); + + /// + /// Initializes a new instance of the class with sensible defaults. + /// + public BenchmarkWorkspaceOptions() + { + Configuration = GetDefaultConfiguration(); + RepositoryPath = DefaultRepositoryPath; + TargetFrameworkMoniker = DefaultTargetFrameworkMoniker; + RepositoryTuningFolder = DefaultRepositoryTuningFolder; + RepositoryReportsFolder = DefaultRepositoryReportsFolder; + BenchmarkProjectSuffix = DefaultBenchmarkProjectSuffix; + } + + /// + /// Gets or sets the root path of the repository where benchmark projects and reports live. + /// + /// The repository root path. + public string RepositoryPath { get; set; } + + /// + /// Gets or sets the instance used by BenchmarkDotNet. + /// + /// The BenchmarkDotNet configuration. + public IConfig Configuration { get; set; } + + /// + /// Gets or sets the target framework moniker (TFM) string to be used for benchmark discovery and reporting. + /// + /// The target framework moniker to run benchmarks for, e.g. net10.0. + public string TargetFrameworkMoniker { get; set; } + + /// + /// Gets or sets the folder name under used to store tuning artifacts. + /// + /// The tuning folder name. Defaults to . + public string RepositoryTuningFolder { get; set; } + + /// + /// Gets or sets the folder name under used to store generated reports. + /// + /// The reports folder name. Defaults to . + public string RepositoryReportsFolder { get; set; } + + /// + /// Gets or sets the project name suffix used to identify benchmark projects. + /// + /// + /// The benchmark project suffix. Defaults to . + /// + public string BenchmarkProjectSuffix { get; set; } + + /// + /// Gets or sets a value indicating whether the benchmarks should be allowed to run using a Debug build. + /// + /// + /// true to allow Debug builds for benchmarks; otherwise, false. Default is false. + /// + public bool AllowDebugBuild { get; set; } + + /// + /// Gets or sets a value indicating whether benchmarks that already have generated reports should be skipped during execution. + /// + /// + /// true to skip benchmarks with existing reports; otherwise, false. Default is false. + /// + public bool SkipBenchmarksWithReports { get; set; } + + /// + /// Finalizes the configured options before use. + /// + /// This method updates the to set the BenchmarkDotNet artifacts path to the combination of and . + public void PostConfigureOptions() + { + if (string.IsNullOrWhiteSpace(Configuration.ArtifactsPath)) + { + var artifactsPath = Path.Combine(RepositoryPath, RepositoryReportsFolder); + + if (Configuration is ManualConfig manual) + { + manual.ArtifactsPath = artifactsPath; + } + else + { + Configuration = Configuration.WithArtifactsPath(artifactsPath); + } + } + } + + /// + /// Determines whether the public read-write properties of this instance are in a valid state. + /// + /// + /// cannot be . + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// + public void ValidateOptions() + { + Validator.ThrowIfInvalidState(Configuration == null); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryPath)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryTuningFolder)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryReportsFolder)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(TargetFrameworkMoniker)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(BenchmarkProjectSuffix)); + } + + private static string GetDefaultRepositoryPath() + { + var dir = AppContext.BaseDirectory; + while (!string.IsNullOrWhiteSpace(dir)) + { + if (Directory.Exists(Path.Combine(dir, ".git"))) + { + return dir; + } + dir = Path.GetDirectoryName(dir); + } + return Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."); + } + + private static ManualConfig GetDefaultConfiguration() + { + var config = ManualConfig.CreateEmpty() + .WithBuildTimeout(TimeSpan.FromMinutes(15)) // for slow machines + .AddLogger(ConsoleLogger.Default) // log output to console + .AddValidator(DefaultConfig.Instance.GetValidators().ToArray()) // copy default validators + .AddAnalyser(DefaultConfig.Instance.GetAnalysers().ToArray()) // copy default analysers + .AddExporter(MarkdownExporter.GitHub) // export to GitHub markdown + .AddColumnProvider(DefaultColumnProviders.Instance) // display default columns (method name, args etc) + .AddJob(Slim.AsDefault()) // tell BDN that this are our default settings + .AddDiagnoser(MemoryDiagnoser.Default) // MemoryDiagnoser is enabled by default + .AddColumn(StatisticColumn.Median, StatisticColumn.Min, StatisticColumn.Max) + .WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(36)); // the default is 20 and trims too aggressively some benchmark results + config.Options = ConfigOptions.DisableLogFile; + return config; + } + + private static Job GetDefaultConfiguredJob() + { + return Job.Default + .WithWarmupCount(1) // 1 warmup is enough for our purpose + .WithIterationTime(TimeInterval.FromMilliseconds(250)) // the default is 0.5s per iteration, which is slightly too much for us + .WithMinIterationCount(15) + .WithMaxIterationCount(20) // we don't want to run more than 20 iterations + .DontEnforcePowerPlan(); // make sure BDN does not try to enforce High Performance power plan on Windows + } + private static string ParseTargetFrameworkMoniker(string frameworkName) - { - if (string.IsNullOrEmpty(frameworkName)) - { - return null; - } - - try - { - var fn = new FrameworkName(frameworkName); - var v = fn.Version; - - // .NET Framework → net11, net20, net35, net40, net403, net45, net451, ..., net48, net481 - if (fn.Identifier.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase)) - { - // Base: net4{minor} or net{major}{minor} for older ones - var tfm = $"net{v.Major}{v.Minor}"; - - // For 4.x: append Build when present (4.0.3 → net403, 4.5.1 → net451, 4.8.1 → net481) - if (v.Major >= 4 && v.Build > 0) - { - tfm += v.Build; - } - - return tfm; - } - - // .NET Standard → netstandard1.0–2.1 - if (fn.Identifier.Equals(".NETStandard", StringComparison.OrdinalIgnoreCase)) - { - return $"netstandard{v.Major}.{v.Minor}"; - } - - // .NET Core / .NET (CoreApp) - if (fn.Identifier.Equals(".NETCoreApp", StringComparison.OrdinalIgnoreCase)) - { - // .NET Core 1.0–3.1 use netcoreappX.Y - if (v.Major <= 3) - { - return $"netcoreapp{v.Major}.{v.Minor}"; - } - - // .NET 5+ uses netX.Y - return $"net{v.Major}.{v.Minor}"; - } - } - catch - { - // ignore invalid framework names - } - - return null; - } - - private static string ResolveCurrentTfm() - { - try - { - var entry = Assembly.GetEntryAssembly(); - var tfa = entry?.GetCustomAttribute(); - if (!string.IsNullOrEmpty(tfa?.FrameworkName)) - { - var result = ParseTargetFrameworkMoniker(tfa.FrameworkName); - if (result != null) - { - return result; - } - } - } - catch - { - // ignore and fallback to dir inspection - } - - try - { - var baseDir = AppContext.BaseDirectory; - var dir = new DirectoryInfo(baseDir); - while (dir != null) - { - if (dir.Name.StartsWith("net", StringComparison.OrdinalIgnoreCase)) - { - return dir.Name; - } - dir = dir.Parent; - } - } - catch - { - // ignore and fallback to null - } - - return null; - } -} + { + if (string.IsNullOrEmpty(frameworkName)) + { + return null; + } + + try + { + var fn = new FrameworkName(frameworkName); + var v = fn.Version; + + // .NET Framework → net11, net20, net35, net40, net403, net45, net451, ..., net48, net481 + if (fn.Identifier.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase)) + { + // Base: net4{minor} or net{major}{minor} for older ones + var tfm = $"net{v.Major}{v.Minor}"; + + // For 4.x: append Build when present (4.0.3 → net403, 4.5.1 → net451, 4.8.1 → net481) + if (v.Major >= 4 && v.Build > 0) + { + tfm += v.Build; + } + + return tfm; + } + + // .NET Standard → netstandard1.0–2.1 + if (fn.Identifier.Equals(".NETStandard", StringComparison.OrdinalIgnoreCase)) + { + return $"netstandard{v.Major}.{v.Minor}"; + } + + // .NET Core / .NET (CoreApp) + if (fn.Identifier.Equals(".NETCoreApp", StringComparison.OrdinalIgnoreCase)) + { + // .NET Core 1.0–3.1 use netcoreappX.Y + if (v.Major <= 3) + { + return $"netcoreapp{v.Major}.{v.Minor}"; + } + + // .NET 5+ uses netX.Y + return $"net{v.Major}.{v.Minor}"; + } + } + catch + { + // ignore invalid framework names + } + + return null; + } + + private static string ResolveCurrentTfm() + { + try + { + var entry = Assembly.GetEntryAssembly(); + var tfa = entry?.GetCustomAttribute(); + if (!string.IsNullOrEmpty(tfa?.FrameworkName)) + { + var result = ParseTargetFrameworkMoniker(tfa.FrameworkName); + if (result != null) + { + return result; + } + } + } + catch + { + // ignore and fallback to dir inspection + } + + try + { + var baseDir = AppContext.BaseDirectory; + var dir = new DirectoryInfo(baseDir); + while (dir != null) + { + if (dir.Name.StartsWith("net", StringComparison.OrdinalIgnoreCase)) + { + return dir.Name; + } + dir = dir.Parent; + } + } + catch + { + // ignore and fallback to null + } + + return null; + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs index a16659a..c164229 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs @@ -1,78 +1,77 @@ -using System; -using BenchmarkDotNet.Configs; -using Cuemon; - -namespace Codebelt.Extensions.BenchmarkDotNet -{ - /// - /// Extension methods for the class. - /// - public static class BenchmarkWorkspaceOptionsExtensions - { - /// - /// Configures the BenchmarkDotNet configuration for the specified . - /// - /// The to extend. - /// The function delegate that configures the . - /// The instance for chaining. - /// - /// cannot be null -or- - /// cannot be null. - /// - /// - /// must not return null. - /// - /// - /// - /// BenchmarkDotNet's configuration model is intentionally immutable-ish: methods such as - /// AddJob, AddColumn, and AddDiagnoser do not mutate the - /// incoming instance. Instead, they produce and return a new - /// configuration object. This behavior is powerful but can be unintuitive when used inside - /// option delegates where callers naturally expect fluent configuration to modify the - /// underlying options instance. - /// - /// - /// Without this helper, callers would need to explicitly reassign: - /// - /// - /// options.Configuration = options.Configuration.AddJob(job); - /// - /// - /// This extension method abstracts away that requirement by: - /// - /// - /// - /// Forcing initialization of the default configuration if needed, - /// - /// - /// Passing the current configuration to the delegate for fluent BenchmarkDotNet operations, - /// - /// - /// Assigning the delegate’s returned configuration back to . - /// - /// - /// - /// This preserves BenchmarkDotNet's design while providing an intuitive experience for users configuring via delegates. - /// - /// - /// - /// cannot be null -or- - /// cannot be null. - /// - /// - /// must not return null. - /// - public static BenchmarkWorkspaceOptions ConfigureBenchmarkDotNet(this BenchmarkWorkspaceOptions options, Func configure) - { - Validator.ThrowIfNull(options); - Validator.ThrowIfNull(configure); - - var current = options.Configuration; // forces default config if needed - var updated = configure(current) ?? throw new InvalidOperationException("Configuration delegate must not return null."); - - options.Configuration = updated; - - return options; - } - } -} +using System; +using BenchmarkDotNet.Configs; +using Cuemon; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Extension methods for the class. +/// +public static class BenchmarkWorkspaceOptionsExtensions +{ + /// + /// Configures the BenchmarkDotNet configuration for the specified . + /// + /// The to extend. + /// The function delegate that configures the . + /// The instance for chaining. + /// + /// cannot be null -or- + /// cannot be null. + /// + /// + /// must not return null. + /// + /// + /// + /// BenchmarkDotNet's configuration model is intentionally immutable-ish: methods such as + /// AddJob, AddColumn, and AddDiagnoser do not mutate the + /// incoming instance. Instead, they produce and return a new + /// configuration object. This behavior is powerful but can be unintuitive when used inside + /// option delegates where callers naturally expect fluent configuration to modify the + /// underlying options instance. + /// + /// + /// Without this helper, callers would need to explicitly reassign: + /// + /// + /// options.Configuration = options.Configuration.AddJob(job); + /// + /// + /// This extension method abstracts away that requirement by: + /// + /// + /// + /// Forcing initialization of the default configuration if needed, + /// + /// + /// Passing the current configuration to the delegate for fluent BenchmarkDotNet operations, + /// + /// + /// Assigning the delegate’s returned configuration back to . + /// + /// + /// + /// This preserves BenchmarkDotNet's design while providing an intuitive experience for users configuring via delegates. + /// + /// + /// + /// cannot be null -or- + /// cannot be null. + /// + /// + /// must not return null. + /// + public static BenchmarkWorkspaceOptions ConfigureBenchmarkDotNet(this BenchmarkWorkspaceOptions options, Func configure) + { + Validator.ThrowIfNull(options); + Validator.ThrowIfNull(configure); + + var current = options.Configuration; // forces default config if needed + var updated = configure(current) ?? throw new InvalidOperationException("Configuration delegate must not return null."); + + options.Configuration = updated; + + return options; + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs index 69798b7..17f18a5 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs @@ -1,8 +1,8 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Using abstractions in public APIs promotes encapsulation, testability, and long-term maintainability, as prescribed by the .NET Framework Design Guidelines.", Scope = "member", Target = "~M:Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspace.LoadAssemblies(System.String,System.String,System.String,System.String,System.Boolean)~System.Collections.Generic.IEnumerable{System.Reflection.Assembly}")] +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Using abstractions in public APIs promotes encapsulation, testability, and long-term maintainability, as prescribed by the .NET Framework Design Guidelines.", Scope = "member", Target = "~M:Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspace.LoadAssemblies(System.String,System.String,System.String,System.String,System.Boolean)~System.Collections.Generic.IEnumerable{System.Reflection.Assembly}")] diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs index b340c46..753176a 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs @@ -1,20 +1,20 @@ -using System.Reflection; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -/// -/// Defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet. -/// -public interface IBenchmarkWorkspace -{ - /// - /// Loads assemblies that contain BenchmarkDotNet benchmarks. - /// - /// An array of instances representing the loaded benchmark assemblies. - Assembly[] LoadBenchmarkAssemblies(); - - /// - /// Performs post-processing on artifacts produced by BenchmarkDotNet. - /// - void PostProcessArtifacts(); -} +using System.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet. +/// +public interface IBenchmarkWorkspace +{ + /// + /// Loads assemblies that contain BenchmarkDotNet benchmarks. + /// + /// An array of instances representing the loaded benchmark assemblies. + Assembly[] LoadBenchmarkAssemblies(); + + /// + /// Performs post-processing on artifacts produced by BenchmarkDotNet. + /// + void PostProcessArtifacts(); +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs index 8030e1f..944b75c 100644 --- a/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs +++ b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs @@ -1,45 +1,44 @@ -using System; -using Cuemon; -using Microsoft.Extensions.DependencyInjection; - -namespace Codebelt.Extensions.BenchmarkDotNet -{ - /// - /// Extension methods for the interface. - /// - public static class ServiceCollectionExtensions - { - /// - /// Adds the default benchmark workspace implementation () to the specified - /// - /// The to add the services to. - /// The which may be configured. - /// The original instance for chaining. - public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection services, Action setup = null) - { - return AddBenchmarkWorkspace(services, setup); - } - - /// - /// Adds a benchmark workspace implementation of type to the specified - /// - /// The type that implements the interface. - /// The to add the services to. - /// The which may be configured. - /// The original instance for chaining. - /// - /// Validates the parameter and the provided configurator. - /// Registers as a singleton using , - /// applies the provided configuration, and registers the resolved as a singleton. - /// - public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection services, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace - { - Validator.ThrowIfNull(services); - Validator.ThrowIfInvalidConfigurator(setup, out var options); - return services - .AddSingleton() - .Configure(setup ?? (_ => {})) - .AddSingleton(options); - } - } -} +using System; +using Cuemon; +using Microsoft.Extensions.DependencyInjection; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Extension methods for the interface. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds the default benchmark workspace implementation () to the specified + /// + /// The to add the services to. + /// The which may be configured. + /// The original instance for chaining. + public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection services, Action setup = null) + { + return AddBenchmarkWorkspace(services, setup); + } + + /// + /// Adds a benchmark workspace implementation of type to the specified + /// + /// The type that implements the interface. + /// The to add the services to. + /// The which may be configured. + /// The original instance for chaining. + /// + /// Validates the parameter and the provided configurator. + /// Registers as a singleton using , + /// applies the provided configuration, and registers the resolved as a singleton. + /// + public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection services, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + Validator.ThrowIfNull(services); + Validator.ThrowIfInvalidConfigurator(setup, out var options); + return services + .AddSingleton() + .Configure(setup ?? (_ => { })) + .AddSingleton(options); + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.FunctionalTests/BenchmarkProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.FunctionalTests/BenchmarkProgramTest.cs index 36c23ec..0858b36 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Console.FunctionalTests/BenchmarkProgramTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.FunctionalTests/BenchmarkProgramTest.cs @@ -1,356 +1,356 @@ -using BenchmarkDotNet.Configs; -using Codebelt.Extensions.Xunit; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.IO; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + /// /// Functional tests for that exercise the full hosted-application lifecycle /// including , , /// and the benchmark-filter pipeline (SkipBenchmarksWithReports). /// public class BenchmarkProgramTest : Test -{ - public BenchmarkProgramTest(ITestOutputHelper output) : base(output) - { - } - - // --------------------------------------------------------------------------- - // RunAsync – direct instantiation tests (avoid starting the full host) - // --------------------------------------------------------------------------- - - [Fact] - public async Task RunAsync_ShouldComplete_WhenWorkspaceReturnsNoAssemblies_AndArgsAreEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var workspace = new EmptyWorkspace(); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act – empty assemblies + empty args → empty foreach → no BenchmarkRunner call - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - Assert.True(workspace.PostProcessArtifactsCalled, "PostProcessArtifacts should always be called in the finally block."); - - TestOutput.WriteLine("RunAsync completed cleanly with empty workspace and no args."); - } - - [Fact] - public async Task RunAsync_ShouldUseBenchmarkSwitcher_WhenArgsAreNonEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var workspace = new EmptyWorkspace(); - - // "--list flat" asks BenchmarkSwitcher to list (not run) benchmarks; safe with an empty assembly list. - var context = new BenchmarkContext(new[] { "--list", "flat" }); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act – BenchmarkSwitcher path (context.Args.Length > 0) - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - Assert.True(workspace.PostProcessArtifactsCalled); - - TestOutput.WriteLine("RunAsync completed cleanly via BenchmarkSwitcher path."); - } - - [Fact] - public async Task RunAsync_ShouldSkipFiltering_WhenSkipBenchmarksWithReportsIsFalse() - { - // Arrange - var options = new BenchmarkWorkspaceOptions { SkipBenchmarksWithReports = false }; - var workspace = new EmptyWorkspace(); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert – no filtering attempted, completes normally - Assert.Null(exception); - } - - [Fact] - public async Task RunAsync_ShouldApplyFilters_WhenSkipBenchmarksWithReportsIsTrue_AndTuningDirDoesNotExist() - { - // Arrange – tuning dir does not exist → ApplyReportFilters early-returns - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var config = ManualConfig.CreateEmpty().WithArtifactsPath(Path.Combine(tempPath, "artifacts")); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning", - SkipBenchmarksWithReports = true - }; - - // Return the functional-test assembly so benchmarkTypes is populated with KnownBenchmark - var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act – tuning dir absent → ApplyReportFilters returns config unchanged - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } - - [Fact] - public async Task RunAsync_ShouldApplyFilters_WhenSkipBenchmarksWithReportsIsTrue_AndMatchingReportExists() - { - // Arrange – a report file whose name contains "KnownBenchmark" exists in the tuning dir. - // KnownBenchmark (defined below) is in this assembly so it will be found by benchmarkTypes. - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - Directory.CreateDirectory(tuningDir); - - // Report file whose name encodes the KnownBenchmark type - File.WriteAllText( - Path.Combine(tuningDir, "Codebelt.Extensions.BenchmarkDotNet.Console.KnownBenchmark-report-github.md"), - "# benchmark report"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning", - SkipBenchmarksWithReports = true - }; - - var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act – filter IS applied for KnownBenchmark (matchingType != null path) - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - - TestOutput.WriteLine("Filter applied for KnownBenchmark report."); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } - - [Fact] - public async Task RunAsync_ShouldSkipReport_WhenFilenameHasLeadingDash() - { - // Arrange – a file whose name starts with "-" causes Split('-').FirstOrDefault() == "" - // which exercises the first 'return null' branch in FindMatchingBenchmarkType. - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - Directory.CreateDirectory(tuningDir); - - File.WriteAllText(Path.Combine(tuningDir, "-leading-dash-report.md"), "edge case"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning", - SkipBenchmarksWithReports = true - }; - - var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert – no filter applied (FindMatchingBenchmarkType returned null), no exception - Assert.Null(exception); - - TestOutput.WriteLine("Leading-dash filename correctly produced null match."); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } - - [Fact] - public async Task RunAsync_ShouldSkipReport_WhenFilenameHasEmptyTypePart() - { - // Arrange – ".-something.md" → potentialTypeFullName = "." → Split('.').LastOrDefault() == "" - // which exercises the second 'return null' branch in FindMatchingBenchmarkType. - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - Directory.CreateDirectory(tuningDir); - - File.WriteAllText(Path.Combine(tuningDir, ".-empty-type-part.md"), "edge case"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning", - SkipBenchmarksWithReports = true - }; - - var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - - TestOutput.WriteLine("Dot-only type-name segment correctly produced null match."); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } - - [Fact] - public async Task RunAsync_ShouldSkipReport_WhenFilenameDoesNotMatchAnyBenchmarkType() - { - // Arrange – well-formed filename but no matching type in the assembly exercises - // the 'return null' path where FirstOrDefault returns null from benchmarkTypes. - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - Directory.CreateDirectory(tuningDir); - - File.WriteAllText( - Path.Combine(tuningDir, "SomeNamespace.NonExistentBenchmark-report.md"), - "no matching type"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning", - SkipBenchmarksWithReports = true - }; - - var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); - var context = new BenchmarkContext(Array.Empty()); - - using var sp = BuildServiceProvider(options, workspace, context); - var program = new BenchmarkProgram(); - - // Act - var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); - - // Assert - Assert.Null(exception); - - TestOutput.WriteLine("No matching benchmark type found – report correctly skipped."); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } - - // --------------------------------------------------------------------------- - // BenchmarkProgram.Run – full hosted-application lifecycle tests - // --------------------------------------------------------------------------- - - [Fact] - public void Run_WithFakeWorkspace_ShouldCompleteWithoutException() - { - // Exercises the static Run(string[], Action) overload (line 82) - // and the full 4-param generic Run (lines 97-105), which starts the host, calls RunAsync, - // and stops the host after RunAsync returns. - // Using named parameter 'setup' disambiguates from the 3-param overload. - var exception = Record.Exception(() => - BenchmarkProgram.Run(Array.Empty(), setup: (Action)null)); - - Assert.Null(exception); - TestOutput.WriteLine("Run completed without exception."); - } - - [Fact] - public void Run_WithFakeWorkspace_AndServiceConfigurator_ShouldInvokeConfigurator() - { - // Exercises Run(string[], Action, Action) - // and verifies that the serviceConfigurator delegate is invoked (line 102). - var configuratorWasCalled = false; - - var exception = Record.Exception(() => - BenchmarkProgram.Run( - Array.Empty(), - serviceConfigurator: services => - { - configuratorWasCalled = true; - services.AddSingleton(_ => null!); // harmless extra registration - })); - - Assert.Null(exception); - Assert.True(configuratorWasCalled, "serviceConfigurator should have been invoked during host setup."); - TestOutput.WriteLine("serviceConfigurator was invoked as expected."); - } - - [Fact] - public void Run_WithFakeWorkspace_AndNonEmptyArgs_ShouldUseBenchmarkSwitcherPath() - { - // Passing "--list flat" exercises the BenchmarkSwitcher branch in ExecuteBenchmarks. - // Using named parameter 'setup' disambiguates from the 3-param overload. - var exception = Record.Exception(() => - BenchmarkProgram.Run(new[] { "--list", "flat" }, setup: (Action)null)); - +{ + public BenchmarkProgramTest(ITestOutputHelper output) : base(output) + { + } + + // --------------------------------------------------------------------------- + // RunAsync – direct instantiation tests (avoid starting the full host) + // --------------------------------------------------------------------------- + + [Fact] + public async Task RunAsync_ShouldComplete_WhenWorkspaceReturnsNoAssemblies_AndArgsAreEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var workspace = new EmptyWorkspace(); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act – empty assemblies + empty args → empty foreach → no BenchmarkRunner call + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + Assert.True(workspace.PostProcessArtifactsCalled, "PostProcessArtifacts should always be called in the finally block."); + + TestOutput.WriteLine("RunAsync completed cleanly with empty workspace and no args."); + } + + [Fact] + public async Task RunAsync_ShouldUseBenchmarkSwitcher_WhenArgsAreNonEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var workspace = new EmptyWorkspace(); + + // "--list flat" asks BenchmarkSwitcher to list (not run) benchmarks; safe with an empty assembly list. + var context = new BenchmarkContext(new[] { "--list", "flat" }); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act – BenchmarkSwitcher path (context.Args.Length > 0) + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + Assert.True(workspace.PostProcessArtifactsCalled); + + TestOutput.WriteLine("RunAsync completed cleanly via BenchmarkSwitcher path."); + } + + [Fact] + public async Task RunAsync_ShouldSkipFiltering_WhenSkipBenchmarksWithReportsIsFalse() + { + // Arrange + var options = new BenchmarkWorkspaceOptions { SkipBenchmarksWithReports = false }; + var workspace = new EmptyWorkspace(); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert – no filtering attempted, completes normally + Assert.Null(exception); + } + + [Fact] + public async Task RunAsync_ShouldApplyFilters_WhenSkipBenchmarksWithReportsIsTrue_AndTuningDirDoesNotExist() + { + // Arrange – tuning dir does not exist → ApplyReportFilters early-returns + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var config = ManualConfig.CreateEmpty().WithArtifactsPath(Path.Combine(tempPath, "artifacts")); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning", + SkipBenchmarksWithReports = true + }; + + // Return the functional-test assembly so benchmarkTypes is populated with KnownBenchmark + var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act – tuning dir absent → ApplyReportFilters returns config unchanged + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } + + [Fact] + public async Task RunAsync_ShouldApplyFilters_WhenSkipBenchmarksWithReportsIsTrue_AndMatchingReportExists() + { + // Arrange – a report file whose name contains "KnownBenchmark" exists in the tuning dir. + // KnownBenchmark (defined below) is in this assembly so it will be found by benchmarkTypes. + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + Directory.CreateDirectory(tuningDir); + + // Report file whose name encodes the KnownBenchmark type + File.WriteAllText( + Path.Combine(tuningDir, "Codebelt.Extensions.BenchmarkDotNet.Console.KnownBenchmark-report-github.md"), + "# benchmark report"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning", + SkipBenchmarksWithReports = true + }; + + var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act – filter IS applied for KnownBenchmark (matchingType != null path) + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + + TestOutput.WriteLine("Filter applied for KnownBenchmark report."); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } + + [Fact] + public async Task RunAsync_ShouldSkipReport_WhenFilenameHasLeadingDash() + { + // Arrange – a file whose name starts with "-" causes Split('-').FirstOrDefault() == "" + // which exercises the first 'return null' branch in FindMatchingBenchmarkType. + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + Directory.CreateDirectory(tuningDir); + + File.WriteAllText(Path.Combine(tuningDir, "-leading-dash-report.md"), "edge case"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning", + SkipBenchmarksWithReports = true + }; + + var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert – no filter applied (FindMatchingBenchmarkType returned null), no exception + Assert.Null(exception); + + TestOutput.WriteLine("Leading-dash filename correctly produced null match."); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } + + [Fact] + public async Task RunAsync_ShouldSkipReport_WhenFilenameHasEmptyTypePart() + { + // Arrange – ".-something.md" → potentialTypeFullName = "." → Split('.').LastOrDefault() == "" + // which exercises the second 'return null' branch in FindMatchingBenchmarkType. + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + Directory.CreateDirectory(tuningDir); + + File.WriteAllText(Path.Combine(tuningDir, ".-empty-type-part.md"), "edge case"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning", + SkipBenchmarksWithReports = true + }; + + var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + + TestOutput.WriteLine("Dot-only type-name segment correctly produced null match."); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } + + [Fact] + public async Task RunAsync_ShouldSkipReport_WhenFilenameDoesNotMatchAnyBenchmarkType() + { + // Arrange – well-formed filename but no matching type in the assembly exercises + // the 'return null' path where FirstOrDefault returns null from benchmarkTypes. + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + Directory.CreateDirectory(tuningDir); + + File.WriteAllText( + Path.Combine(tuningDir, "SomeNamespace.NonExistentBenchmark-report.md"), + "no matching type"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning", + SkipBenchmarksWithReports = true + }; + + var workspace = new AssemblyWorkspace(typeof(BenchmarkProgramTest).Assembly); + var context = new BenchmarkContext(Array.Empty()); + + using var sp = BuildServiceProvider(options, workspace, context); + var program = new BenchmarkProgram(); + + // Act + var exception = await Record.ExceptionAsync(() => program.RunAsync(sp, CancellationToken.None)); + + // Assert + Assert.Null(exception); + + TestOutput.WriteLine("No matching benchmark type found – report correctly skipped."); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } + + // --------------------------------------------------------------------------- + // BenchmarkProgram.Run – full hosted-application lifecycle tests + // --------------------------------------------------------------------------- + + [Fact] + public void Run_WithFakeWorkspace_ShouldCompleteWithoutException() + { + // Exercises the static Run(string[], Action) overload (line 82) + // and the full 4-param generic Run (lines 97-105), which starts the host, calls RunAsync, + // and stops the host after RunAsync returns. + // Using named parameter 'setup' disambiguates from the 3-param overload. + var exception = Record.Exception(() => + BenchmarkProgram.Run(Array.Empty(), setup: (Action)null)); + + Assert.Null(exception); + TestOutput.WriteLine("Run completed without exception."); + } + + [Fact] + public void Run_WithFakeWorkspace_AndServiceConfigurator_ShouldInvokeConfigurator() + { + // Exercises Run(string[], Action, Action) + // and verifies that the serviceConfigurator delegate is invoked (line 102). + var configuratorWasCalled = false; + + var exception = Record.Exception(() => + BenchmarkProgram.Run( + Array.Empty(), + serviceConfigurator: services => + { + configuratorWasCalled = true; + services.AddSingleton(_ => null!); // harmless extra registration + })); + + Assert.Null(exception); + Assert.True(configuratorWasCalled, "serviceConfigurator should have been invoked during host setup."); + TestOutput.WriteLine("serviceConfigurator was invoked as expected."); + } + + [Fact] + public void Run_WithFakeWorkspace_AndNonEmptyArgs_ShouldUseBenchmarkSwitcherPath() + { + // Passing "--list flat" exercises the BenchmarkSwitcher branch in ExecuteBenchmarks. + // Using named parameter 'setup' disambiguates from the 3-param overload. + var exception = Record.Exception(() => + BenchmarkProgram.Run(new[] { "--list", "flat" }, setup: (Action)null)); + Assert.Null(exception); TestOutput.WriteLine("Run with '--list flat' completed (BenchmarkSwitcher path)."); } @@ -400,56 +400,56 @@ public async Task RunAsync_WithFakeWorkspace_AndNonEmptyArgs_ShouldUseBenchmarkS // --------------------------------------------------------------------------- // Helper: build a minimal service provider for RunAsync tests - // --------------------------------------------------------------------------- - - private static ServiceProvider BuildServiceProvider( - BenchmarkWorkspaceOptions options, - IBenchmarkWorkspace workspace, - BenchmarkContext context) - { - return new ServiceCollection() - .AddSingleton(workspace) - .AddSingleton(options) - .AddSingleton(context) - .BuildServiceProvider(); - } - - // --------------------------------------------------------------------------- - // Test doubles - // --------------------------------------------------------------------------- - - /// - /// A workspace that returns an empty assembly array and tracks whether - /// was called. - /// - private sealed class EmptyWorkspace : IBenchmarkWorkspace - { - public bool PostProcessArtifactsCalled { get; private set; } - - public Assembly[] LoadBenchmarkAssemblies() => Array.Empty(); - - public void PostProcessArtifacts() => PostProcessArtifactsCalled = true; - } - - /// - /// A workspace that returns a specific assembly so that benchmark-type discovery - /// (filtering by types whose names end with "Benchmark") finds . - /// - private sealed class AssemblyWorkspace : IBenchmarkWorkspace - { - private readonly Assembly _assembly; - - public AssemblyWorkspace(Assembly assembly) => _assembly = assembly; - - public Assembly[] LoadBenchmarkAssemblies() => new[] { _assembly }; - - public void PostProcessArtifacts() { } - } -} - -/// -/// A placeholder type whose name ends with "Benchmark" so that -/// 's filter logic can discover it via -/// t.Name.EndsWith("Benchmark"). -/// -internal sealed class KnownBenchmark { } + // --------------------------------------------------------------------------- + + private static ServiceProvider BuildServiceProvider( + BenchmarkWorkspaceOptions options, + IBenchmarkWorkspace workspace, + BenchmarkContext context) + { + return new ServiceCollection() + .AddSingleton(workspace) + .AddSingleton(options) + .AddSingleton(context) + .BuildServiceProvider(); + } + + // --------------------------------------------------------------------------- + // Test doubles + // --------------------------------------------------------------------------- + + /// + /// A workspace that returns an empty assembly array and tracks whether + /// was called. + /// + private sealed class EmptyWorkspace : IBenchmarkWorkspace + { + public bool PostProcessArtifactsCalled { get; private set; } + + public Assembly[] LoadBenchmarkAssemblies() => Array.Empty(); + + public void PostProcessArtifacts() => PostProcessArtifactsCalled = true; + } + + /// + /// A workspace that returns a specific assembly so that benchmark-type discovery + /// (filtering by types whose names end with "Benchmark") finds . + /// + private sealed class AssemblyWorkspace : IBenchmarkWorkspace + { + private readonly Assembly _assembly; + + public AssemblyWorkspace(Assembly assembly) => _assembly = assembly; + + public Assembly[] LoadBenchmarkAssemblies() => new[] { _assembly }; + + public void PostProcessArtifacts() { } + } +} + +/// +/// A placeholder type whose name ends with "Benchmark" so that +/// 's filter logic can discover it via +/// t.Name.EndsWith("Benchmark"). +/// +internal sealed class KnownBenchmark { } diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs index 5df38f3..148684c 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs @@ -1,223 +1,223 @@ -using Codebelt.Extensions.Xunit; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -public class BenchmarkContextTest : Test -{ - public BenchmarkContextTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void Constructor_ShouldInitializeWithEmptyArray_WhenArgsIsNull() - { - // Act - var context = new BenchmarkContext(null); - - // Assert - Assert.NotNull(context.Args); - Assert.Empty(context.Args); - } - - [Fact] - public void Constructor_ShouldInitializeWithEmptyArray_WhenArgsIsEmpty() - { - // Arrange - var emptyArgs = new string[] { }; - - // Act - var context = new BenchmarkContext(emptyArgs); - - // Assert - Assert.NotNull(context.Args); - Assert.Empty(context.Args); - Assert.Same(emptyArgs, context.Args); - } - - [Fact] - public void Constructor_ShouldInitializeWithProvidedArgs_WhenArgsIsNotEmpty() - { - // Arrange - var args = new[] { "arg1", "arg2", "arg3" }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(3, context.Args.Length); - Assert.Same(args, context.Args); - } - - [Fact] - public void Args_ShouldReturnOriginalArray_WhenProvided() - { - // Arrange - var args = new[] { "--filter", "MyBenchmark", "--job", "short" }; - var context = new BenchmarkContext(args); - - // Act - var result = context.Args; - - // Assert - Assert.Same(args, result); - Assert.Equal(4, result.Length); - Assert.Equal("--filter", result[0]); - Assert.Equal("MyBenchmark", result[1]); - Assert.Equal("--job", result[2]); - Assert.Equal("short", result[3]); - } - - [Fact] - public void Args_ShouldBeReadOnly_ButArrayContentsAreMutable() - { - // Arrange - var args = new[] { "original" }; - var context = new BenchmarkContext(args); - - // Act - Modify the original array - args[0] = "modified"; - - // Assert - The property reflects the change (no defensive copy) - Assert.Equal("modified", context.Args[0]); - } - - [Fact] - public void Constructor_ShouldHandleSingleArgument() - { - // Arrange - var args = new[] { "single-arg" }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Single(context.Args); - Assert.Equal("single-arg", context.Args[0]); - } - - [Fact] - public void Constructor_ShouldHandleArgsWithSpecialCharacters() - { - // Arrange - var args = new[] { "--config", "Debug", "--output", "C:\\Reports\\benchmark-results.txt" }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(4, context.Args.Length); - Assert.Equal("--config", context.Args[0]); - Assert.Equal("Debug", context.Args[1]); - Assert.Equal("--output", context.Args[2]); - Assert.Equal("C:\\Reports\\benchmark-results.txt", context.Args[3]); - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(5)] - [InlineData(10)] - public void Constructor_ShouldHandleVariousArrayLengths(int length) - { - // Arrange - var args = new string[length]; - for (int i = 0; i < length; i++) - { - args[i] = $"arg{i}"; - } - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(length, context.Args.Length); - - TestOutput.WriteLine($"Created context with {length} arguments"); - } - - [Fact] - public void Constructor_ShouldHandleArgsWithEmptyStrings() - { - // Arrange - var args = new[] { "", "valid", "" }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(3, context.Args.Length); - Assert.Equal("", context.Args[0]); - Assert.Equal("valid", context.Args[1]); - Assert.Equal("", context.Args[2]); - } - - [Fact] - public void Constructor_ShouldHandleArgsWithWhitespace() - { - // Arrange - var args = new[] { " ", "valid", "\t\t" }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(3, context.Args.Length); - Assert.Equal(" ", context.Args[0]); - Assert.Equal("valid", context.Args[1]); - Assert.Equal("\t\t", context.Args[2]); - } - - [Fact] - public void Args_ShouldReturnEmptyArray_AfterConstructorWithNull() - { - // Arrange - var context = new BenchmarkContext(null); - - // Act - var args = context.Args; - - // Assert - Assert.NotNull(args); - Assert.Empty(args); - Assert.IsType(args); - } - - [Fact] - public void Constructor_ShouldHandleTypicalBenchmarkDotNetArgs() - { - // Arrange - var args = new[] - { - "--filter", - "Codebelt.Extensions.BenchmarkDotNet.*", - "--job", - "short", - "--exporters", - "markdown", - "--memory" - }; - - // Act - var context = new BenchmarkContext(args); - - // Assert - Assert.NotNull(context.Args); - Assert.Equal(7, context.Args.Length); - Assert.Equal("--filter", context.Args[0]); - Assert.Equal("Codebelt.Extensions.BenchmarkDotNet.*", context.Args[1]); - Assert.Equal("--job", context.Args[2]); - Assert.Equal("short", context.Args[3]); - Assert.Equal("--exporters", context.Args[4]); - Assert.Equal("markdown", context.Args[5]); - Assert.Equal("--memory", context.Args[6]); - - TestOutput.WriteLine($"Successfully created context with {context.Args.Length} BenchmarkDotNet arguments"); - } -} +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +public class BenchmarkContextTest : Test +{ + public BenchmarkContextTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldInitializeWithEmptyArray_WhenArgsIsNull() + { + // Act + var context = new BenchmarkContext(null); + + // Assert + Assert.NotNull(context.Args); + Assert.Empty(context.Args); + } + + [Fact] + public void Constructor_ShouldInitializeWithEmptyArray_WhenArgsIsEmpty() + { + // Arrange + var emptyArgs = new string[] { }; + + // Act + var context = new BenchmarkContext(emptyArgs); + + // Assert + Assert.NotNull(context.Args); + Assert.Empty(context.Args); + Assert.Same(emptyArgs, context.Args); + } + + [Fact] + public void Constructor_ShouldInitializeWithProvidedArgs_WhenArgsIsNotEmpty() + { + // Arrange + var args = new[] { "arg1", "arg2", "arg3" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(3, context.Args.Length); + Assert.Same(args, context.Args); + } + + [Fact] + public void Args_ShouldReturnOriginalArray_WhenProvided() + { + // Arrange + var args = new[] { "--filter", "MyBenchmark", "--job", "short" }; + var context = new BenchmarkContext(args); + + // Act + var result = context.Args; + + // Assert + Assert.Same(args, result); + Assert.Equal(4, result.Length); + Assert.Equal("--filter", result[0]); + Assert.Equal("MyBenchmark", result[1]); + Assert.Equal("--job", result[2]); + Assert.Equal("short", result[3]); + } + + [Fact] + public void Args_ShouldBeReadOnly_ButArrayContentsAreMutable() + { + // Arrange + var args = new[] { "original" }; + var context = new BenchmarkContext(args); + + // Act - Modify the original array + args[0] = "modified"; + + // Assert - The property reflects the change (no defensive copy) + Assert.Equal("modified", context.Args[0]); + } + + [Fact] + public void Constructor_ShouldHandleSingleArgument() + { + // Arrange + var args = new[] { "single-arg" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Single(context.Args); + Assert.Equal("single-arg", context.Args[0]); + } + + [Fact] + public void Constructor_ShouldHandleArgsWithSpecialCharacters() + { + // Arrange + var args = new[] { "--config", "Debug", "--output", "C:\\Reports\\benchmark-results.txt" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(4, context.Args.Length); + Assert.Equal("--config", context.Args[0]); + Assert.Equal("Debug", context.Args[1]); + Assert.Equal("--output", context.Args[2]); + Assert.Equal("C:\\Reports\\benchmark-results.txt", context.Args[3]); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + [InlineData(10)] + public void Constructor_ShouldHandleVariousArrayLengths(int length) + { + // Arrange + var args = new string[length]; + for (int i = 0; i < length; i++) + { + args[i] = $"arg{i}"; + } + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(length, context.Args.Length); + + TestOutput.WriteLine($"Created context with {length} arguments"); + } + + [Fact] + public void Constructor_ShouldHandleArgsWithEmptyStrings() + { + // Arrange + var args = new[] { "", "valid", "" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(3, context.Args.Length); + Assert.Equal("", context.Args[0]); + Assert.Equal("valid", context.Args[1]); + Assert.Equal("", context.Args[2]); + } + + [Fact] + public void Constructor_ShouldHandleArgsWithWhitespace() + { + // Arrange + var args = new[] { " ", "valid", "\t\t" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(3, context.Args.Length); + Assert.Equal(" ", context.Args[0]); + Assert.Equal("valid", context.Args[1]); + Assert.Equal("\t\t", context.Args[2]); + } + + [Fact] + public void Args_ShouldReturnEmptyArray_AfterConstructorWithNull() + { + // Arrange + var context = new BenchmarkContext(null); + + // Act + var args = context.Args; + + // Assert + Assert.NotNull(args); + Assert.Empty(args); + Assert.IsType(args); + } + + [Fact] + public void Constructor_ShouldHandleTypicalBenchmarkDotNetArgs() + { + // Arrange + var args = new[] + { + "--filter", + "Codebelt.Extensions.BenchmarkDotNet.*", + "--job", + "short", + "--exporters", + "markdown", + "--memory" + }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(7, context.Args.Length); + Assert.Equal("--filter", context.Args[0]); + Assert.Equal("Codebelt.Extensions.BenchmarkDotNet.*", context.Args[1]); + Assert.Equal("--job", context.Args[2]); + Assert.Equal("short", context.Args[3]); + Assert.Equal("--exporters", context.Args[4]); + Assert.Equal("markdown", context.Args[5]); + Assert.Equal("--memory", context.Args[6]); + + TestOutput.WriteLine($"Successfully created context with {context.Args.Length} BenchmarkDotNet arguments"); + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs index 67a8964..3291f51 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs @@ -4,349 +4,349 @@ using System.Linq; using System.Threading.Tasks; using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -public class BenchmarkProgramTest : Test -{ - public BenchmarkProgramTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void BuildConfiguration_ShouldReturnValidValue() - { - // Act - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - - // Assert - Assert.NotNull(buildConfiguration); - Assert.True(buildConfiguration == "Debug" || buildConfiguration == "Release"); - - TestOutput.WriteLine($"BuildConfiguration: {buildConfiguration}"); - } - - [Fact] - public void IsDebugBuild_ShouldBeConsistentWithBuildConfiguration() - { - // Act - var isDebugBuild = BenchmarkProgram.IsDebugBuild; - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - - // Assert - if (buildConfiguration == "Debug") - { - Assert.True(isDebugBuild); - } - else if (buildConfiguration == "Release") - { - Assert.False(isDebugBuild); - } - - TestOutput.WriteLine($"IsDebugBuild: {isDebugBuild}, BuildConfiguration: {buildConfiguration}"); - } - - [Fact] - public void BuildConfiguration_ShouldBeDebug_WhenCompiledInDebugMode() - { - // Arrange - var entryAssembly = Assembly.GetEntryAssembly(); - if (entryAssembly == null) - { - // Skip test if entry assembly is not available (e.g., when running in certain test environments) - TestOutput.WriteLine("Entry assembly is null, skipping test"); - return; - } - - if (entryAssembly.FullName.Contains("ReSharper", StringComparison.OrdinalIgnoreCase)) - { - // Skip test if running under ReSharper - TestOutput.WriteLine("Running under ReSharper, skipping test"); - return; - } - - // Act - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - - // Assert - #if DEBUG - Assert.Equal("Debug", buildConfiguration); - #else - Assert.Equal("Release", buildConfiguration); - #endif - - TestOutput.WriteLine($"BuildConfiguration: {buildConfiguration}"); - } - - [Fact] - public void IsDebugBuild_ShouldBeTrue_WhenCompiledInDebugMode() - { - // Arrange - var entryAssembly = Assembly.GetEntryAssembly(); - if (entryAssembly == null) - { - // Skip test if entry assembly is not available - TestOutput.WriteLine("Entry assembly is null, skipping test"); - return; - } - - if (entryAssembly.FullName.Contains("ReSharper", StringComparison.OrdinalIgnoreCase)) - { - // Skip test if running under ReSharper - TestOutput.WriteLine("Running under ReSharper, skipping test"); - return; - } - - // Act - var isDebugBuild = BenchmarkProgram.IsDebugBuild; - - // Assert - #if DEBUG - Assert.True(isDebugBuild); - #else - Assert.False(isDebugBuild); - #endif - - TestOutput.WriteLine($"IsDebugBuild: {isDebugBuild}"); - } - - [Fact] - public void BuildConfiguration_ShouldBeCached_AcrossMultipleCalls() - { - // Act - var firstCall = BenchmarkProgram.BuildConfiguration; - var secondCall = BenchmarkProgram.BuildConfiguration; - var thirdCall = BenchmarkProgram.BuildConfiguration; - - // Assert - Assert.Equal(firstCall, secondCall); - Assert.Equal(secondCall, thirdCall); - Assert.Same(firstCall, secondCall); // Should be the same string instance - - TestOutput.WriteLine($"BuildConfiguration called 3 times, all returned: {firstCall}"); - } - - [Fact] - public void IsDebugBuild_ShouldBeCached_AcrossMultipleCalls() - { - // Act - var firstCall = BenchmarkProgram.IsDebugBuild; - var secondCall = BenchmarkProgram.IsDebugBuild; - var thirdCall = BenchmarkProgram.IsDebugBuild; - - // Assert - Assert.Equal(firstCall, secondCall); - Assert.Equal(secondCall, thirdCall); - - TestOutput.WriteLine($"IsDebugBuild called 3 times, all returned: {firstCall}"); - } - - [Fact] - public void StaticProperties_ShouldBeInitializedAtStartup() - { - // Act & Assert - Static constructor should have already run - Assert.NotNull(BenchmarkProgram.BuildConfiguration); - Assert.NotEmpty(BenchmarkProgram.BuildConfiguration); - - // IsDebugBuild can be true or false, but should be set - var isDebugBuild = BenchmarkProgram.IsDebugBuild; - Assert.True(isDebugBuild == true || isDebugBuild == false); - - TestOutput.WriteLine($"BuildConfiguration: {BenchmarkProgram.BuildConfiguration}"); - TestOutput.WriteLine($"IsDebugBuild: {BenchmarkProgram.IsDebugBuild}"); - } - - [Theory] - [InlineData("Debug", true)] - [InlineData("Release", false)] - public void BuildConfiguration_ShouldMatchExpectedDebugState(string expectedConfig, bool expectedIsDebug) - { - // Act - var actualConfig = BenchmarkProgram.BuildConfiguration; - var actualIsDebug = BenchmarkProgram.IsDebugBuild; - - // Assert - if (actualConfig == expectedConfig) - { - Assert.Equal(expectedIsDebug, actualIsDebug); - TestOutput.WriteLine($"Configuration '{actualConfig}' correctly maps to IsDebugBuild={actualIsDebug}"); - } - else - { - TestOutput.WriteLine($"Skipping assertion - actual configuration is '{actualConfig}', not '{expectedConfig}'"); - } - } - - [Fact] - public void BuildConfiguration_ShouldNotBeNullOrEmpty() - { - // Act - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - - // Assert - Assert.NotNull(buildConfiguration); - Assert.NotEmpty(buildConfiguration); - Assert.False(string.IsNullOrWhiteSpace(buildConfiguration)); - - TestOutput.WriteLine($"BuildConfiguration: '{buildConfiguration}'"); - } - - [Fact] - public void BuildConfiguration_ShouldBeOneOfTwoValidValues() - { - // Act - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - - // Assert - Assert.Contains(buildConfiguration, new[] { "Debug", "Release" }); - - TestOutput.WriteLine($"BuildConfiguration is valid: {buildConfiguration}"); - } - - [Fact] - public void IsDebugBuild_ShouldBeBoolean() - { - // Act - var isDebugBuild = BenchmarkProgram.IsDebugBuild; - - // Assert - Assert.IsType(isDebugBuild); - - TestOutput.WriteLine($"IsDebugBuild type verified: {isDebugBuild}"); - } - - [Fact] - public void StaticConstructor_ShouldSetPropertiesBasedOnEntryAssembly() - { - // This test verifies that the static constructor logic has executed correctly - // by checking that both properties are set and consistent with each other - - // Act - var buildConfiguration = BenchmarkProgram.BuildConfiguration; - var isDebugBuild = BenchmarkProgram.IsDebugBuild; - - // Assert - Assert.NotNull(buildConfiguration); - - if (isDebugBuild) - { - Assert.Equal("Debug", buildConfiguration); - } - else - { - Assert.Equal("Release", buildConfiguration); - } - - TestOutput.WriteLine($"Static properties correctly initialized - BuildConfiguration: {buildConfiguration}, IsDebugBuild: {isDebugBuild}"); - } - - [Fact] - public void BenchmarkProgram_ShouldInheritFromConsoleProgram() - { - // Act - var baseType = typeof(BenchmarkProgram).BaseType; - - // Assert - Assert.NotNull(baseType); - Assert.True(baseType.IsGenericType); - Assert.Equal("MinimalConsoleProgram`1", baseType.Name); - - TestOutput.WriteLine($"BenchmarkProgram correctly inherits from: {baseType.FullName}"); - } - - [Fact] - public void BenchmarkProgram_ShouldUseCorrectGenericTypeParameter() - { - // Act - var baseType = typeof(BenchmarkProgram).BaseType; - var genericArguments = baseType?.GetGenericArguments(); - - // Assert - Assert.NotNull(genericArguments); - Assert.Single(genericArguments); - Assert.Equal(typeof(BenchmarkProgram), genericArguments[0]); - - TestOutput.WriteLine($"BenchmarkProgram uses correct generic type parameter: {genericArguments[0].Name}"); - } - - [Fact] - public void BenchmarkProgram_ShouldBePublicClass() - { - // Act - var type = typeof(BenchmarkProgram); - - // Assert - Assert.True(type.IsPublic); - Assert.True(type.IsClass); - Assert.False(type.IsAbstract); - Assert.False(type.IsSealed); - - TestOutput.WriteLine($"BenchmarkProgram is a public, non-abstract, non-sealed class"); - } - - [Fact] - public void Run_MethodWithDefaultWorkspace_ShouldExist() - { - // Act - var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => - m.Name == "Run" && - !m.IsGenericMethodDefinition && - m.GetParameters().Length == 2 && - m.GetParameters()[0].ParameterType == typeof(string[]) && - m.GetParameters()[1].ParameterType == typeof(Action)); - - // Assert - Assert.NotNull(method); - Assert.True(method.IsStatic); - Assert.True(method.IsPublic); - Assert.Equal(typeof(void), method.ReturnType); - - TestOutput.WriteLine($"Found Run method with default workspace: {method}"); - } - - [Fact] - public void Run_GenericMethod_ShouldExist() - { - // Act - var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => - m.Name == "Run" && - m.IsGenericMethodDefinition && - m.GetParameters().Length == 2); - - // Assert - Assert.NotNull(method); - Assert.True(method.IsStatic); - Assert.True(method.IsPublic); - Assert.True(method.IsGenericMethodDefinition); - Assert.Equal(typeof(void), method.ReturnType); - - var genericArguments = method.GetGenericArguments(); - Assert.Single(genericArguments); - - TestOutput.WriteLine($"Found generic Run method: {method}"); - } - - [Fact] + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +public class BenchmarkProgramTest : Test +{ + public BenchmarkProgramTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void BuildConfiguration_ShouldReturnValidValue() + { + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.NotNull(buildConfiguration); + Assert.True(buildConfiguration == "Debug" || buildConfiguration == "Release"); + + TestOutput.WriteLine($"BuildConfiguration: {buildConfiguration}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeConsistentWithBuildConfiguration() + { + // Act + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + if (buildConfiguration == "Debug") + { + Assert.True(isDebugBuild); + } + else if (buildConfiguration == "Release") + { + Assert.False(isDebugBuild); + } + + TestOutput.WriteLine($"IsDebugBuild: {isDebugBuild}, BuildConfiguration: {buildConfiguration}"); + } + + [Fact] + public void BuildConfiguration_ShouldBeDebug_WhenCompiledInDebugMode() + { + // Arrange + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly == null) + { + // Skip test if entry assembly is not available (e.g., when running in certain test environments) + TestOutput.WriteLine("Entry assembly is null, skipping test"); + return; + } + + if (entryAssembly.FullName.Contains("ReSharper", StringComparison.OrdinalIgnoreCase)) + { + // Skip test if running under ReSharper + TestOutput.WriteLine("Running under ReSharper, skipping test"); + return; + } + + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert +#if DEBUG + Assert.Equal("Debug", buildConfiguration); +#else + Assert.Equal("Release", buildConfiguration); +#endif + + TestOutput.WriteLine($"BuildConfiguration: {buildConfiguration}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeTrue_WhenCompiledInDebugMode() + { + // Arrange + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly == null) + { + // Skip test if entry assembly is not available + TestOutput.WriteLine("Entry assembly is null, skipping test"); + return; + } + + if (entryAssembly.FullName.Contains("ReSharper", StringComparison.OrdinalIgnoreCase)) + { + // Skip test if running under ReSharper + TestOutput.WriteLine("Running under ReSharper, skipping test"); + return; + } + + // Act + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + + // Assert +#if DEBUG + Assert.True(isDebugBuild); +#else + Assert.False(isDebugBuild); +#endif + + TestOutput.WriteLine($"IsDebugBuild: {isDebugBuild}"); + } + + [Fact] + public void BuildConfiguration_ShouldBeCached_AcrossMultipleCalls() + { + // Act + var firstCall = BenchmarkProgram.BuildConfiguration; + var secondCall = BenchmarkProgram.BuildConfiguration; + var thirdCall = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.Equal(firstCall, secondCall); + Assert.Equal(secondCall, thirdCall); + Assert.Same(firstCall, secondCall); // Should be the same string instance + + TestOutput.WriteLine($"BuildConfiguration called 3 times, all returned: {firstCall}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeCached_AcrossMultipleCalls() + { + // Act + var firstCall = BenchmarkProgram.IsDebugBuild; + var secondCall = BenchmarkProgram.IsDebugBuild; + var thirdCall = BenchmarkProgram.IsDebugBuild; + + // Assert + Assert.Equal(firstCall, secondCall); + Assert.Equal(secondCall, thirdCall); + + TestOutput.WriteLine($"IsDebugBuild called 3 times, all returned: {firstCall}"); + } + + [Fact] + public void StaticProperties_ShouldBeInitializedAtStartup() + { + // Act & Assert - Static constructor should have already run + Assert.NotNull(BenchmarkProgram.BuildConfiguration); + Assert.NotEmpty(BenchmarkProgram.BuildConfiguration); + + // IsDebugBuild can be true or false, but should be set + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + Assert.True(isDebugBuild == true || isDebugBuild == false); + + TestOutput.WriteLine($"BuildConfiguration: {BenchmarkProgram.BuildConfiguration}"); + TestOutput.WriteLine($"IsDebugBuild: {BenchmarkProgram.IsDebugBuild}"); + } + + [Theory] + [InlineData("Debug", true)] + [InlineData("Release", false)] + public void BuildConfiguration_ShouldMatchExpectedDebugState(string expectedConfig, bool expectedIsDebug) + { + // Act + var actualConfig = BenchmarkProgram.BuildConfiguration; + var actualIsDebug = BenchmarkProgram.IsDebugBuild; + + // Assert + if (actualConfig == expectedConfig) + { + Assert.Equal(expectedIsDebug, actualIsDebug); + TestOutput.WriteLine($"Configuration '{actualConfig}' correctly maps to IsDebugBuild={actualIsDebug}"); + } + else + { + TestOutput.WriteLine($"Skipping assertion - actual configuration is '{actualConfig}', not '{expectedConfig}'"); + } + } + + [Fact] + public void BuildConfiguration_ShouldNotBeNullOrEmpty() + { + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.NotNull(buildConfiguration); + Assert.NotEmpty(buildConfiguration); + Assert.False(string.IsNullOrWhiteSpace(buildConfiguration)); + + TestOutput.WriteLine($"BuildConfiguration: '{buildConfiguration}'"); + } + + [Fact] + public void BuildConfiguration_ShouldBeOneOfTwoValidValues() + { + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.Contains(buildConfiguration, new[] { "Debug", "Release" }); + + TestOutput.WriteLine($"BuildConfiguration is valid: {buildConfiguration}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeBoolean() + { + // Act + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + + // Assert + Assert.IsType(isDebugBuild); + + TestOutput.WriteLine($"IsDebugBuild type verified: {isDebugBuild}"); + } + + [Fact] + public void StaticConstructor_ShouldSetPropertiesBasedOnEntryAssembly() + { + // This test verifies that the static constructor logic has executed correctly + // by checking that both properties are set and consistent with each other + + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + + // Assert + Assert.NotNull(buildConfiguration); + + if (isDebugBuild) + { + Assert.Equal("Debug", buildConfiguration); + } + else + { + Assert.Equal("Release", buildConfiguration); + } + + TestOutput.WriteLine($"Static properties correctly initialized - BuildConfiguration: {buildConfiguration}, IsDebugBuild: {isDebugBuild}"); + } + + [Fact] + public void BenchmarkProgram_ShouldInheritFromConsoleProgram() + { + // Act + var baseType = typeof(BenchmarkProgram).BaseType; + + // Assert + Assert.NotNull(baseType); + Assert.True(baseType.IsGenericType); + Assert.Equal("MinimalConsoleProgram`1", baseType.Name); + + TestOutput.WriteLine($"BenchmarkProgram correctly inherits from: {baseType.FullName}"); + } + + [Fact] + public void BenchmarkProgram_ShouldUseCorrectGenericTypeParameter() + { + // Act + var baseType = typeof(BenchmarkProgram).BaseType; + var genericArguments = baseType?.GetGenericArguments(); + + // Assert + Assert.NotNull(genericArguments); + Assert.Single(genericArguments); + Assert.Equal(typeof(BenchmarkProgram), genericArguments[0]); + + TestOutput.WriteLine($"BenchmarkProgram uses correct generic type parameter: {genericArguments[0].Name}"); + } + + [Fact] + public void BenchmarkProgram_ShouldBePublicClass() + { + // Act + var type = typeof(BenchmarkProgram); + + // Assert + Assert.True(type.IsPublic); + Assert.True(type.IsClass); + Assert.False(type.IsAbstract); + Assert.False(type.IsSealed); + + TestOutput.WriteLine($"BenchmarkProgram is a public, non-abstract, non-sealed class"); + } + + [Fact] + public void Run_MethodWithDefaultWorkspace_ShouldExist() + { + // Act + var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + !m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[0].ParameterType == typeof(string[]) && + m.GetParameters()[1].ParameterType == typeof(Action)); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsStatic); + Assert.True(method.IsPublic); + Assert.Equal(typeof(void), method.ReturnType); + + TestOutput.WriteLine($"Found Run method with default workspace: {method}"); + } + + [Fact] + public void Run_GenericMethod_ShouldExist() + { + // Act + var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + m.IsGenericMethodDefinition && + m.GetParameters().Length == 2); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsStatic); + Assert.True(method.IsPublic); + Assert.True(method.IsGenericMethodDefinition); + Assert.Equal(typeof(void), method.ReturnType); + + var genericArguments = method.GetGenericArguments(); + Assert.Single(genericArguments); + + TestOutput.WriteLine($"Found generic Run method: {method}"); + } + + [Fact] public void Run_GenericMethod_ShouldHaveCorrectConstraints() { // Act var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => - m.Name == "Run" && - m.IsGenericMethodDefinition && - m.GetParameters().Length == 2); - - Assert.NotNull(method); - - var genericArguments = method.GetGenericArguments(); - var typeParameter = genericArguments[0]; - - // Assert - var constraints = typeParameter.GetGenericParameterConstraints(); - Assert.Contains(constraints, c => c == typeof(IBenchmarkWorkspace)); - Assert.True(typeParameter.GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)); + .FirstOrDefault(m => + m.Name == "Run" && + m.IsGenericMethodDefinition && + m.GetParameters().Length == 2); + + Assert.NotNull(method); + + var genericArguments = method.GetGenericArguments(); + var typeParameter = genericArguments[0]; + + // Assert + var constraints = typeParameter.GetGenericParameterConstraints(); + Assert.Contains(constraints, c => c == typeof(IBenchmarkWorkspace)); + Assert.True(typeParameter.GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)); TestOutput.WriteLine($"Generic type parameter '{typeParameter.Name}' has correct constraints: class, IBenchmarkWorkspace"); } @@ -421,63 +421,63 @@ public void RunAsync_GenericMethod_ShouldHaveCorrectConstraints() [Fact] public void StaticProperties_ShouldBeReadOnly() { - // Act - var buildConfigProperty = typeof(BenchmarkProgram).GetProperty("BuildConfiguration"); - var isDebugBuildProperty = typeof(BenchmarkProgram).GetProperty("IsDebugBuild"); - - // Assert - Assert.NotNull(buildConfigProperty); - Assert.True(buildConfigProperty.CanRead); - Assert.False(buildConfigProperty.CanWrite); - - Assert.NotNull(isDebugBuildProperty); - Assert.True(isDebugBuildProperty.CanRead); - Assert.False(isDebugBuildProperty.CanWrite); - - TestOutput.WriteLine("Both static properties are read-only as expected"); - } - - [Fact] - public void Run_Methods_ShouldHaveCorrectParameterNames() - { - // Act - Get the non-generic Run method explicitly - var nonGenericMethod = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => - m.Name == "Run" && - !m.IsGenericMethodDefinition && - m.GetParameters().Length == 2 && - m.GetParameters()[0].ParameterType == typeof(string[]) && - m.GetParameters()[1].ParameterType == typeof(Action)); - - // Assert - Assert.NotNull(nonGenericMethod); - - var parameters = nonGenericMethod.GetParameters(); - Assert.Equal(2, parameters.Length); - Assert.Equal("args", parameters[0].Name); - Assert.Equal("setup", parameters[1].Name); - - TestOutput.WriteLine($"Run method parameters: {string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"))}"); - } - - [Fact] + // Act + var buildConfigProperty = typeof(BenchmarkProgram).GetProperty("BuildConfiguration"); + var isDebugBuildProperty = typeof(BenchmarkProgram).GetProperty("IsDebugBuild"); + + // Assert + Assert.NotNull(buildConfigProperty); + Assert.True(buildConfigProperty.CanRead); + Assert.False(buildConfigProperty.CanWrite); + + Assert.NotNull(isDebugBuildProperty); + Assert.True(isDebugBuildProperty.CanRead); + Assert.False(isDebugBuildProperty.CanWrite); + + TestOutput.WriteLine("Both static properties are read-only as expected"); + } + + [Fact] + public void Run_Methods_ShouldHaveCorrectParameterNames() + { + // Act - Get the non-generic Run method explicitly + var nonGenericMethod = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + !m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[0].ParameterType == typeof(string[]) && + m.GetParameters()[1].ParameterType == typeof(Action)); + + // Assert + Assert.NotNull(nonGenericMethod); + + var parameters = nonGenericMethod.GetParameters(); + Assert.Equal(2, parameters.Length); + Assert.Equal("args", parameters[0].Name); + Assert.Equal("setup", parameters[1].Name); + + TestOutput.WriteLine($"Run method parameters: {string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"))}"); + } + + [Fact] public void Run_Methods_ShouldHaveOptionalSetupParameter() { // Act - Get the non-generic Run method explicitly var nonGenericMethod = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => - m.Name == "Run" && - !m.IsGenericMethodDefinition && - m.GetParameters().Length == 2 && - m.GetParameters()[0].ParameterType == typeof(string[]) && - m.GetParameters()[1].ParameterType == typeof(Action)); - - // Assert - Assert.NotNull(nonGenericMethod); - - var setupParameter = nonGenericMethod.GetParameters()[1]; - Assert.True(setupParameter.IsOptional); - Assert.Null(setupParameter.DefaultValue); + .FirstOrDefault(m => + m.Name == "Run" && + !m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[0].ParameterType == typeof(string[]) && + m.GetParameters()[1].ParameterType == typeof(Action)); + + // Assert + Assert.NotNull(nonGenericMethod); + + var setupParameter = nonGenericMethod.GetParameters()[1]; + Assert.True(setupParameter.IsOptional); + Assert.Null(setupParameter.DefaultValue); TestOutput.WriteLine($"Setup parameter is optional with default value: {setupParameter.DefaultValue ?? "null"}"); } diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkProgramTest.cs index 19128e7..42917e9 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkProgramTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkProgramTest.cs @@ -1,145 +1,145 @@ -using BenchmarkDotNet.Configs; -using Codebelt.Extensions.BenchmarkDotNet.Console; -using Codebelt.Extensions.Xunit; -using System; -using System.IO; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -/// -/// Functional tests for non-generic overloads and end-to-end host lifecycle -/// using the real against the existing tuning assemblies. -/// -public class BenchmarkProgramTest : Test -{ - // BenchmarkProgram.IsDebugBuild reflects whether the entry assembly was compiled in Debug mode, - // which is what BenchmarkWorkspace uses to locate tuning assemblies under bin/Debug or bin/Release. - private static readonly bool IsDebugBuild = BenchmarkProgram.IsDebugBuild; - - public BenchmarkProgramTest(ITestOutputHelper output) : base(output) - { - } - - /// - /// Exercises the non-generic Run(string[], Action<BenchmarkWorkspaceOptions>) overload (line 54) - /// and the non-generic Run(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>) - /// overload (line 68) which both ultimately delegate to - /// Run<BenchmarkWorkspace>(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>). - /// - /// Using "--list flat" makes BenchmarkSwitcher enumerate available benchmarks and return immediately - /// without executing any, which keeps the test fast and deterministic. - /// - [Fact] - public void Run_NonGeneric_WithListFlatArg_ShouldCompleteWithoutException() - { - var exception = Record.Exception(() => - BenchmarkProgram.Run( - new[] { "--list", "flat" }, - options => - { - options.AllowDebugBuild = IsDebugBuild; - options.Configuration = ManualConfig.CreateEmpty() - .WithOptions(ConfigOptions.DisableLogFile | ConfigOptions.DisableOptimizationsValidator); - })); - - Assert.Null(exception); - TestOutput.WriteLine("Non-generic BenchmarkProgram.Run completed via '--list flat'."); - } - - /// - /// Exercises the non-generic Run(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>) - /// overload with an explicit to ensure the three-argument - /// non-generic overload (line 68) is reached. - /// - [Fact] - public void Run_NonGenericWithServiceConfigurator_WithListFlatArg_ShouldCompleteWithoutException() - { - var configuratorCalled = false; - - var exception = Record.Exception(() => - BenchmarkProgram.Run( - new[] { "--list", "flat" }, - services => { configuratorCalled = true; }, - options => - { - options.AllowDebugBuild = IsDebugBuild; - options.Configuration = ManualConfig.CreateEmpty() - .WithOptions(ConfigOptions.DisableLogFile | ConfigOptions.DisableOptimizationsValidator); - })); - - Assert.Null(exception); - Assert.True(configuratorCalled); - TestOutput.WriteLine("Non-generic BenchmarkProgram.Run with service configurator completed."); - } - - /// - /// Verifies that loads the tuning assemblies - /// built for the current configuration and TFM, and that the assembly resolver hook is registered. - /// Running this test with the real workspace may cause the - /// event to fire for transitive dependencies discovered only through the tuning-folder DLL scan. - /// - [Fact] - public void BenchmarkWorkspace_LoadBenchmarkAssemblies_ShouldLoadTuningAssemblies() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.NotEmpty(assemblies); - - foreach (var assembly in assemblies) - { - TestOutput.WriteLine($"Loaded: {assembly.GetName().Name}"); - } - } - - /// - /// Verifies that behaves correctly when the - /// results directory already has files and then calls delete. This exercises the full cleanup path. - /// - [Fact] - public void BenchmarkWorkspace_PostProcessArtifacts_ShouldMoveFilesAndDeleteResultsDir() - { - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - Directory.CreateDirectory(resultsDir); - - File.WriteAllText(Path.Combine(resultsDir, "SomeBenchmark-report.md"), "# report"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - workspace.PostProcessArtifacts(); - - // Assert - Assert.False(Directory.Exists(resultsDir)); - Assert.True(File.Exists(Path.Combine(tuningDir, "SomeBenchmark-report.md"))); - } - finally - { - if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); - } - } -} +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.BenchmarkDotNet.Console; +using Codebelt.Extensions.Xunit; +using System; +using System.IO; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Functional tests for non-generic overloads and end-to-end host lifecycle +/// using the real against the existing tuning assemblies. +/// +public class BenchmarkProgramTest : Test +{ + // BenchmarkProgram.IsDebugBuild reflects whether the entry assembly was compiled in Debug mode, + // which is what BenchmarkWorkspace uses to locate tuning assemblies under bin/Debug or bin/Release. + private static readonly bool IsDebugBuild = BenchmarkProgram.IsDebugBuild; + + public BenchmarkProgramTest(ITestOutputHelper output) : base(output) + { + } + + /// + /// Exercises the non-generic Run(string[], Action<BenchmarkWorkspaceOptions>) overload (line 54) + /// and the non-generic Run(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>) + /// overload (line 68) which both ultimately delegate to + /// Run<BenchmarkWorkspace>(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>). + /// + /// Using "--list flat" makes BenchmarkSwitcher enumerate available benchmarks and return immediately + /// without executing any, which keeps the test fast and deterministic. + /// + [Fact] + public void Run_NonGeneric_WithListFlatArg_ShouldCompleteWithoutException() + { + var exception = Record.Exception(() => + BenchmarkProgram.Run( + new[] { "--list", "flat" }, + options => + { + options.AllowDebugBuild = IsDebugBuild; + options.Configuration = ManualConfig.CreateEmpty() + .WithOptions(ConfigOptions.DisableLogFile | ConfigOptions.DisableOptimizationsValidator); + })); + + Assert.Null(exception); + TestOutput.WriteLine("Non-generic BenchmarkProgram.Run completed via '--list flat'."); + } + + /// + /// Exercises the non-generic Run(string[], Action<IServiceCollection>, Action<BenchmarkWorkspaceOptions>) + /// overload with an explicit to ensure the three-argument + /// non-generic overload (line 68) is reached. + /// + [Fact] + public void Run_NonGenericWithServiceConfigurator_WithListFlatArg_ShouldCompleteWithoutException() + { + var configuratorCalled = false; + + var exception = Record.Exception(() => + BenchmarkProgram.Run( + new[] { "--list", "flat" }, + services => { configuratorCalled = true; }, + options => + { + options.AllowDebugBuild = IsDebugBuild; + options.Configuration = ManualConfig.CreateEmpty() + .WithOptions(ConfigOptions.DisableLogFile | ConfigOptions.DisableOptimizationsValidator); + })); + + Assert.Null(exception); + Assert.True(configuratorCalled); + TestOutput.WriteLine("Non-generic BenchmarkProgram.Run with service configurator completed."); + } + + /// + /// Verifies that loads the tuning assemblies + /// built for the current configuration and TFM, and that the assembly resolver hook is registered. + /// Running this test with the real workspace may cause the + /// event to fire for transitive dependencies discovered only through the tuning-folder DLL scan. + /// + [Fact] + public void BenchmarkWorkspace_LoadBenchmarkAssemblies_ShouldLoadTuningAssemblies() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.NotEmpty(assemblies); + + foreach (var assembly in assemblies) + { + TestOutput.WriteLine($"Loaded: {assembly.GetName().Name}"); + } + } + + /// + /// Verifies that behaves correctly when the + /// results directory already has files and then calls delete. This exercises the full cleanup path. + /// + [Fact] + public void BenchmarkWorkspace_PostProcessArtifacts_ShouldMoveFilesAndDeleteResultsDir() + { + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + Directory.CreateDirectory(resultsDir); + + File.WriteAllText(Path.Combine(resultsDir, "SomeBenchmark-report.md"), "# report"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.True(File.Exists(Path.Combine(tuningDir, "SomeBenchmark-report.md"))); + } + finally + { + if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); + } + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkRunnerProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkRunnerProgramTest.cs index f048084..da4deb8 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkRunnerProgramTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/BenchmarkRunnerProgramTest.cs @@ -42,8 +42,9 @@ public void Program_Main_ShouldConfigureBenchmarkWorkspaceOptions() Assert.Equal(BenchmarkProgram.IsDebugBuild, options.AllowDebugBuild); Assert.True(options.SkipBenchmarksWithReports); - Assert.Contains(jobs, job => job.Environment.Runtime == CoreRuntime.Core90); + Assert.DoesNotContain(jobs, job => job.Environment.Runtime == CoreRuntime.Core90); Assert.Contains(jobs, job => job.Environment.Runtime == CoreRuntime.Core10_0); + Assert.Contains(jobs, job => job.Environment.Runtime?.MsBuildMoniker == "net11.0"); } } #endif diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests.csproj b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests.csproj index c1c3f64..d7bcb28 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests.csproj +++ b/test/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests/Codebelt.Extensions.BenchmarkDotNet.FunctionalTests.csproj @@ -1,13 +1,13 @@ - - - - Codebelt.Extensions.BenchmarkDotNet - - + + + + Codebelt.Extensions.BenchmarkDotNet + + - + diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs index f0d7697..6254002 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs @@ -1,754 +1,754 @@ -using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Configs; using Codebelt.Extensions.Xunit; using System; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -public class BenchmarkWorkspaceOptionsTest : Test -{ - public BenchmarkWorkspaceOptionsTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void Constructor_ShouldInitializeWithDefaultValues() - { - // Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.RepositoryPath); - Assert.NotNull(options.Configuration); - Assert.NotNull(options.TargetFrameworkMoniker); - Assert.Equal(BenchmarkWorkspaceOptions.DefaultRepositoryTuningFolder, options.RepositoryTuningFolder); - Assert.Equal(BenchmarkWorkspaceOptions.DefaultRepositoryReportsFolder, options.RepositoryReportsFolder); - Assert.Equal(BenchmarkWorkspaceOptions.DefaultBenchmarkProjectSuffix, options.BenchmarkProjectSuffix); - Assert.False(options.AllowDebugBuild); - } - - [Fact] - public void DefaultRepositoryReportsFolder_ShouldBeReports() - { - // Assert - Assert.Equal("reports", BenchmarkWorkspaceOptions.DefaultRepositoryReportsFolder); - } - - [Fact] - public void DefaultRepositoryTuningFolder_ShouldBeTuning() - { - // Assert - Assert.Equal("tuning", BenchmarkWorkspaceOptions.DefaultRepositoryTuningFolder); - } - - [Fact] - public void DefaultBenchmarkProjectSuffix_ShouldBeBenchmarks() - { - // Assert - Assert.Equal("Benchmarks", BenchmarkWorkspaceOptions.DefaultBenchmarkProjectSuffix); - } - - [Fact] - public void RepositoryPath_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testPath = @"C:\TestRepo"; - - // Act - options.RepositoryPath = testPath; - - // Assert - Assert.Equal(testPath, options.RepositoryPath); - } - - [Fact] - public void Configuration_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var customConfig = ManualConfig.CreateEmpty(); - - // Act - options.Configuration = customConfig; - - // Assert - Assert.Same(customConfig, options.Configuration); - } - - [Fact] - public void TargetFrameworkMoniker_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testTfm = "net8.0"; - - // Act - options.TargetFrameworkMoniker = testTfm; - - // Assert - Assert.Equal(testTfm, options.TargetFrameworkMoniker); - } - - [Fact] - public void RepositoryTuningFolder_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testFolder = "custom-tuning"; - - // Act - options.RepositoryTuningFolder = testFolder; - - // Assert - Assert.Equal(testFolder, options.RepositoryTuningFolder); - } - - [Fact] - public void RepositoryReportsFolder_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testFolder = "custom-reports"; - - // Act - options.RepositoryReportsFolder = testFolder; - - // Assert - Assert.Equal(testFolder, options.RepositoryReportsFolder); - } - - [Fact] - public void BenchmarkProjectSuffix_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testSuffix = "Perf"; - - // Act - options.BenchmarkProjectSuffix = testSuffix; - - // Assert - Assert.Equal(testSuffix, options.BenchmarkProjectSuffix); - } - - [Fact] - public void UseDebugBuild_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - - // Act - options.AllowDebugBuild = true; - - // Assert - Assert.True(options.AllowDebugBuild); - } - - [Fact] - public void PostConfigureOptions_ShouldCombineRepositoryPathAndReportsFolder() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testRepoPath = @"C:\MyBenchmarks"; - var testReportsFolder = "output"; - options.RepositoryPath = testRepoPath; - options.RepositoryReportsFolder = testReportsFolder; - - // Act - options.PostConfigureOptions(); - - // Assert - var expectedPath = Path.Combine(testRepoPath, testReportsFolder); - Assert.Equal(expectedPath, options.Configuration.ArtifactsPath); - } - - [Fact] - public void ValidateOptions_ShouldNotThrow_WhenAllPropertiesAreValid() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - - // Act & Assert - var exception = Record.Exception(() => options.ValidateOptions()); - Assert.Null(exception); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenConfigurationIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - Configuration = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = string.Empty - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsWhitespace() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = " " - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryTuningFolder = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryTuningFolder = string.Empty - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsWhitespace() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryTuningFolder = " " - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryReportsFolder = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryReportsFolder = string.Empty - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsWhitespace() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryReportsFolder = " " - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - TargetFrameworkMoniker = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - TargetFrameworkMoniker = string.Empty - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsWhitespace() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - TargetFrameworkMoniker = " " - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsNull() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - BenchmarkProjectSuffix = null - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsEmpty() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - BenchmarkProjectSuffix = string.Empty - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsWhitespace() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - BenchmarkProjectSuffix = " " - }; - - // Act & Assert - Assert.Throws(() => options.ValidateOptions()); - } - - [Fact] - public void Constructor_ShouldSetRepositoryPath_ToGitRootOrFallback() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.RepositoryPath); - Assert.NotEmpty(options.RepositoryPath); - - TestOutput.WriteLine($"RepositoryPath: {options.RepositoryPath}"); - } - - [Fact] - public void Constructor_ShouldSetConfiguration_WithMemoryDiagnoser() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.Configuration); - Assert.Contains(options.Configuration.GetDiagnosers(), d => d.GetType().Name.Contains("Memory")); - } - - [Fact] - public void Constructor_ShouldSetConfiguration_WithConsoleLogger() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.Configuration); - Assert.NotEmpty(options.Configuration.GetLoggers()); - } - - [Fact] - public void Constructor_ShouldSetConfiguration_WithMarkdownExporter() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.Configuration); - Assert.NotEmpty(options.Configuration.GetExporters()); - } - - [Fact] - public void Constructor_ShouldSetTargetFrameworkMoniker_ToCurrentRuntime() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.TargetFrameworkMoniker); - Assert.StartsWith("net", options.TargetFrameworkMoniker, StringComparison.OrdinalIgnoreCase); - - TestOutput.WriteLine($"TargetFrameworkMoniker: {options.TargetFrameworkMoniker}"); - } - - [Theory] - [InlineData("net8.0")] - [InlineData("net9.0")] - [InlineData("net10.0")] - public void TargetFrameworkMoniker_ShouldAcceptValidTfmFormats(string tfm) - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - - // Act - options.TargetFrameworkMoniker = tfm; - - // Assert - Assert.Equal(tfm, options.TargetFrameworkMoniker); - } - - [Fact] - public void PostConfigureOptions_ShouldPreserveOtherConfigurationSettings() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var originalLoggers = options.Configuration.GetLoggers(); - var originalExporters = options.Configuration.GetExporters(); - - // Act - options.PostConfigureOptions(); - - // Assert - Assert.Equal(originalLoggers.Count(), options.Configuration.GetLoggers().Count()); - Assert.Equal(originalExporters.Count(), options.Configuration.GetExporters().Count()); - } - - [Fact] - public void UseDebugBuild_ShouldDefaultToFalse() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.False(options.AllowDebugBuild); - } - - [Fact] - public void Slim_ShouldBeConfiguredJob() - { - // Act - var slimJob = BenchmarkWorkspaceOptions.Slim; - - // Assert - Assert.NotNull(slimJob); - Assert.Equal(1, slimJob.Run.WarmupCount); - Assert.Equal(15, slimJob.Run.MinIterationCount); - Assert.Equal(20, slimJob.Run.MaxIterationCount); - Assert.Equal(250, slimJob.Run.IterationTime.ToMilliseconds()); - - TestOutput.WriteLine($"Slim Job - WarmupCount: {slimJob.Run.WarmupCount}"); - TestOutput.WriteLine($"Slim Job - MinIterationCount: {slimJob.Run.MinIterationCount}"); - TestOutput.WriteLine($"Slim Job - MaxIterationCount: {slimJob.Run.MaxIterationCount}"); - TestOutput.WriteLine($"Slim Job - IterationTime: {slimJob.Run.IterationTime.ToMilliseconds()}ms"); - } - - [Fact] - public void PostConfigureOptions_ShouldNotOverwriteExistingArtifactsPath() - { - // Arrange - var existingPath = @"C:\ExistingArtifacts"; - var options = new BenchmarkWorkspaceOptions(); - - if (options.Configuration is ManualConfig manual) - { - manual.ArtifactsPath = existingPath; - } - else - { - options.Configuration = options.Configuration.WithArtifactsPath(existingPath); - } - - // Act - options.PostConfigureOptions(); - - // Assert - Assert.Equal(existingPath, options.Configuration.ArtifactsPath); - } - - [Fact] - public void PostConfigureOptions_ShouldWorkWithNonManualConfig() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - var testRepoPath = @"C:\TestRepo"; - var testReportsFolder = "reports"; - - // Create a non-ManualConfig by using WithArtifactsPath on DefaultConfig - options.Configuration = ManualConfig.CreateEmpty().WithArtifactsPath(""); - options.RepositoryPath = testRepoPath; - options.RepositoryReportsFolder = testReportsFolder; - - // Act - options.PostConfigureOptions(); - - // Assert - var expectedPath = Path.Combine(testRepoPath, testReportsFolder); - Assert.Equal(expectedPath, options.Configuration.ArtifactsPath); - } - - [Fact] - public void Configuration_ShouldHaveStatisticColumns() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - var config = options.Configuration as ManualConfig; - Assert.NotNull(config); - - // Check that column providers were added via the config - var columnProviders = options.Configuration.GetColumnProviders().ToList(); - Assert.NotEmpty(columnProviders); - - // Verify StatisticsColumnProvider is present (which would include our Median, Min, Max columns) - Assert.Contains(columnProviders, provider => provider.GetType().Name == "StatisticsColumnProvider"); - - // The configuration includes DefaultColumnProviders.Instance which adds standard providers - var providerNames = columnProviders.Select(p => p.GetType().Name).ToList(); - TestOutput.WriteLine($"Column Providers: {string.Join(", ", providerNames)}"); - } - - [Fact] - public void Configuration_ShouldHaveCustomSummaryStyle() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotNull(options.Configuration.SummaryStyle); - Assert.Equal(36, options.Configuration.SummaryStyle.MaxParameterColumnWidth); - - TestOutput.WriteLine($"MaxParameterColumnWidth: {options.Configuration.SummaryStyle.MaxParameterColumnWidth}"); - } - - [Fact] - public void Configuration_ShouldHaveDisabledLogFile() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.True((options.Configuration.Options & ConfigOptions.DisableLogFile) == ConfigOptions.DisableLogFile); - } - - [Fact] - public void Configuration_ShouldHaveBuildTimeout() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.Equal(TimeSpan.FromMinutes(15), options.Configuration.BuildTimeout); - - TestOutput.WriteLine($"BuildTimeout: {options.Configuration.BuildTimeout}"); - } - - [Fact] - public void Configuration_ShouldHaveDefaultValidatorsAndAnalysers() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.NotEmpty(options.Configuration.GetValidators()); - Assert.NotEmpty(options.Configuration.GetAnalysers()); - - TestOutput.WriteLine($"Validators: {options.Configuration.GetValidators().Count()}"); - TestOutput.WriteLine($"Analysers: {options.Configuration.GetAnalysers().Count()}"); - } - - [Fact] - public void Configuration_ShouldHaveSlimJobAsDefault() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - var jobs = options.Configuration.GetJobs().ToList(); - Assert.Single(jobs); - Assert.True(jobs[0].Meta.IsDefault); - - TestOutput.WriteLine($"Default Job - WarmupCount: {jobs[0].Run.WarmupCount}"); - } - - [Fact] - public void SkipBenchmarksWithReports_ShouldDefaultToFalse() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - - // Assert - Assert.False(options.SkipBenchmarksWithReports); - } - - [Fact] - public void SkipBenchmarksWithReports_ShouldBeSettable() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - - // Act - options.SkipBenchmarksWithReports = true; - - // Assert - Assert.True(options.SkipBenchmarksWithReports); - } - - [Theory] - [InlineData(".NETFramework,Version=v1.1", "net11")] - [InlineData(".NETFramework,Version=v2.0", "net20")] - [InlineData(".NETFramework,Version=v3.5", "net35")] - [InlineData(".NETFramework,Version=v4.0", "net40")] - [InlineData(".NETFramework,Version=v4.0.3", "net403")] - [InlineData(".NETFramework,Version=v4.5.1", "net451")] - [InlineData(".NETFramework,Version=v4.7.2", "net472")] - [InlineData(".NETFramework,Version=v4.8", "net48")] - [InlineData(".NETFramework,Version=v4.8.1", "net481")] - public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETFramework(string frameworkName, string expectedTfm) - { - // Act + +namespace Codebelt.Extensions.BenchmarkDotNet; + +public class BenchmarkWorkspaceOptionsTest : Test +{ + public BenchmarkWorkspaceOptionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldInitializeWithDefaultValues() + { + // Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.RepositoryPath); + Assert.NotNull(options.Configuration); + Assert.NotNull(options.TargetFrameworkMoniker); + Assert.Equal(BenchmarkWorkspaceOptions.DefaultRepositoryTuningFolder, options.RepositoryTuningFolder); + Assert.Equal(BenchmarkWorkspaceOptions.DefaultRepositoryReportsFolder, options.RepositoryReportsFolder); + Assert.Equal(BenchmarkWorkspaceOptions.DefaultBenchmarkProjectSuffix, options.BenchmarkProjectSuffix); + Assert.False(options.AllowDebugBuild); + } + + [Fact] + public void DefaultRepositoryReportsFolder_ShouldBeReports() + { + // Assert + Assert.Equal("reports", BenchmarkWorkspaceOptions.DefaultRepositoryReportsFolder); + } + + [Fact] + public void DefaultRepositoryTuningFolder_ShouldBeTuning() + { + // Assert + Assert.Equal("tuning", BenchmarkWorkspaceOptions.DefaultRepositoryTuningFolder); + } + + [Fact] + public void DefaultBenchmarkProjectSuffix_ShouldBeBenchmarks() + { + // Assert + Assert.Equal("Benchmarks", BenchmarkWorkspaceOptions.DefaultBenchmarkProjectSuffix); + } + + [Fact] + public void RepositoryPath_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testPath = @"C:\TestRepo"; + + // Act + options.RepositoryPath = testPath; + + // Assert + Assert.Equal(testPath, options.RepositoryPath); + } + + [Fact] + public void Configuration_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var customConfig = ManualConfig.CreateEmpty(); + + // Act + options.Configuration = customConfig; + + // Assert + Assert.Same(customConfig, options.Configuration); + } + + [Fact] + public void TargetFrameworkMoniker_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testTfm = "net8.0"; + + // Act + options.TargetFrameworkMoniker = testTfm; + + // Assert + Assert.Equal(testTfm, options.TargetFrameworkMoniker); + } + + [Fact] + public void RepositoryTuningFolder_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testFolder = "custom-tuning"; + + // Act + options.RepositoryTuningFolder = testFolder; + + // Assert + Assert.Equal(testFolder, options.RepositoryTuningFolder); + } + + [Fact] + public void RepositoryReportsFolder_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testFolder = "custom-reports"; + + // Act + options.RepositoryReportsFolder = testFolder; + + // Assert + Assert.Equal(testFolder, options.RepositoryReportsFolder); + } + + [Fact] + public void BenchmarkProjectSuffix_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testSuffix = "Perf"; + + // Act + options.BenchmarkProjectSuffix = testSuffix; + + // Assert + Assert.Equal(testSuffix, options.BenchmarkProjectSuffix); + } + + [Fact] + public void UseDebugBuild_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act + options.AllowDebugBuild = true; + + // Assert + Assert.True(options.AllowDebugBuild); + } + + [Fact] + public void PostConfigureOptions_ShouldCombineRepositoryPathAndReportsFolder() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testRepoPath = @"C:\MyBenchmarks"; + var testReportsFolder = "output"; + options.RepositoryPath = testRepoPath; + options.RepositoryReportsFolder = testReportsFolder; + + // Act + options.PostConfigureOptions(); + + // Assert + var expectedPath = Path.Combine(testRepoPath, testReportsFolder); + Assert.Equal(expectedPath, options.Configuration.ArtifactsPath); + } + + [Fact] + public void ValidateOptions_ShouldNotThrow_WhenAllPropertiesAreValid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act & Assert + var exception = Record.Exception(() => options.ValidateOptions()); + Assert.Null(exception); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenConfigurationIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + Configuration = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryTuningFolder = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryTuningFolder = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryTuningFolder = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryReportsFolder = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryReportsFolder = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryReportsFolder = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + TargetFrameworkMoniker = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + TargetFrameworkMoniker = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + TargetFrameworkMoniker = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void Constructor_ShouldSetRepositoryPath_ToGitRootOrFallback() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.RepositoryPath); + Assert.NotEmpty(options.RepositoryPath); + + TestOutput.WriteLine($"RepositoryPath: {options.RepositoryPath}"); + } + + [Fact] + public void Constructor_ShouldSetConfiguration_WithMemoryDiagnoser() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.Configuration); + Assert.Contains(options.Configuration.GetDiagnosers(), d => d.GetType().Name.Contains("Memory")); + } + + [Fact] + public void Constructor_ShouldSetConfiguration_WithConsoleLogger() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.Configuration); + Assert.NotEmpty(options.Configuration.GetLoggers()); + } + + [Fact] + public void Constructor_ShouldSetConfiguration_WithMarkdownExporter() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.Configuration); + Assert.NotEmpty(options.Configuration.GetExporters()); + } + + [Fact] + public void Constructor_ShouldSetTargetFrameworkMoniker_ToCurrentRuntime() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.TargetFrameworkMoniker); + Assert.StartsWith("net", options.TargetFrameworkMoniker, StringComparison.OrdinalIgnoreCase); + + TestOutput.WriteLine($"TargetFrameworkMoniker: {options.TargetFrameworkMoniker}"); + } + + [Theory] + [InlineData("net8.0")] + [InlineData("net9.0")] + [InlineData("net10.0")] + public void TargetFrameworkMoniker_ShouldAcceptValidTfmFormats(string tfm) + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act + options.TargetFrameworkMoniker = tfm; + + // Assert + Assert.Equal(tfm, options.TargetFrameworkMoniker); + } + + [Fact] + public void PostConfigureOptions_ShouldPreserveOtherConfigurationSettings() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var originalLoggers = options.Configuration.GetLoggers(); + var originalExporters = options.Configuration.GetExporters(); + + // Act + options.PostConfigureOptions(); + + // Assert + Assert.Equal(originalLoggers.Count(), options.Configuration.GetLoggers().Count()); + Assert.Equal(originalExporters.Count(), options.Configuration.GetExporters().Count()); + } + + [Fact] + public void UseDebugBuild_ShouldDefaultToFalse() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.False(options.AllowDebugBuild); + } + + [Fact] + public void Slim_ShouldBeConfiguredJob() + { + // Act + var slimJob = BenchmarkWorkspaceOptions.Slim; + + // Assert + Assert.NotNull(slimJob); + Assert.Equal(1, slimJob.Run.WarmupCount); + Assert.Equal(15, slimJob.Run.MinIterationCount); + Assert.Equal(20, slimJob.Run.MaxIterationCount); + Assert.Equal(250, slimJob.Run.IterationTime.ToMilliseconds()); + + TestOutput.WriteLine($"Slim Job - WarmupCount: {slimJob.Run.WarmupCount}"); + TestOutput.WriteLine($"Slim Job - MinIterationCount: {slimJob.Run.MinIterationCount}"); + TestOutput.WriteLine($"Slim Job - MaxIterationCount: {slimJob.Run.MaxIterationCount}"); + TestOutput.WriteLine($"Slim Job - IterationTime: {slimJob.Run.IterationTime.ToMilliseconds()}ms"); + } + + [Fact] + public void PostConfigureOptions_ShouldNotOverwriteExistingArtifactsPath() + { + // Arrange + var existingPath = @"C:\ExistingArtifacts"; + var options = new BenchmarkWorkspaceOptions(); + + if (options.Configuration is ManualConfig manual) + { + manual.ArtifactsPath = existingPath; + } + else + { + options.Configuration = options.Configuration.WithArtifactsPath(existingPath); + } + + // Act + options.PostConfigureOptions(); + + // Assert + Assert.Equal(existingPath, options.Configuration.ArtifactsPath); + } + + [Fact] + public void PostConfigureOptions_ShouldWorkWithNonManualConfig() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testRepoPath = @"C:\TestRepo"; + var testReportsFolder = "reports"; + + // Create a non-ManualConfig by using WithArtifactsPath on DefaultConfig + options.Configuration = ManualConfig.CreateEmpty().WithArtifactsPath(""); + options.RepositoryPath = testRepoPath; + options.RepositoryReportsFolder = testReportsFolder; + + // Act + options.PostConfigureOptions(); + + // Assert + var expectedPath = Path.Combine(testRepoPath, testReportsFolder); + Assert.Equal(expectedPath, options.Configuration.ArtifactsPath); + } + + [Fact] + public void Configuration_ShouldHaveStatisticColumns() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + var config = options.Configuration as ManualConfig; + Assert.NotNull(config); + + // Check that column providers were added via the config + var columnProviders = options.Configuration.GetColumnProviders().ToList(); + Assert.NotEmpty(columnProviders); + + // Verify StatisticsColumnProvider is present (which would include our Median, Min, Max columns) + Assert.Contains(columnProviders, provider => provider.GetType().Name == "StatisticsColumnProvider"); + + // The configuration includes DefaultColumnProviders.Instance which adds standard providers + var providerNames = columnProviders.Select(p => p.GetType().Name).ToList(); + TestOutput.WriteLine($"Column Providers: {string.Join(", ", providerNames)}"); + } + + [Fact] + public void Configuration_ShouldHaveCustomSummaryStyle() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.Configuration.SummaryStyle); + Assert.Equal(36, options.Configuration.SummaryStyle.MaxParameterColumnWidth); + + TestOutput.WriteLine($"MaxParameterColumnWidth: {options.Configuration.SummaryStyle.MaxParameterColumnWidth}"); + } + + [Fact] + public void Configuration_ShouldHaveDisabledLogFile() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.True((options.Configuration.Options & ConfigOptions.DisableLogFile) == ConfigOptions.DisableLogFile); + } + + [Fact] + public void Configuration_ShouldHaveBuildTimeout() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.Equal(TimeSpan.FromMinutes(15), options.Configuration.BuildTimeout); + + TestOutput.WriteLine($"BuildTimeout: {options.Configuration.BuildTimeout}"); + } + + [Fact] + public void Configuration_ShouldHaveDefaultValidatorsAndAnalysers() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotEmpty(options.Configuration.GetValidators()); + Assert.NotEmpty(options.Configuration.GetAnalysers()); + + TestOutput.WriteLine($"Validators: {options.Configuration.GetValidators().Count()}"); + TestOutput.WriteLine($"Analysers: {options.Configuration.GetAnalysers().Count()}"); + } + + [Fact] + public void Configuration_ShouldHaveSlimJobAsDefault() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + var jobs = options.Configuration.GetJobs().ToList(); + Assert.Single(jobs); + Assert.True(jobs[0].Meta.IsDefault); + + TestOutput.WriteLine($"Default Job - WarmupCount: {jobs[0].Run.WarmupCount}"); + } + + [Fact] + public void SkipBenchmarksWithReports_ShouldDefaultToFalse() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.False(options.SkipBenchmarksWithReports); + } + + [Fact] + public void SkipBenchmarksWithReports_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act + options.SkipBenchmarksWithReports = true; + + // Assert + Assert.True(options.SkipBenchmarksWithReports); + } + + [Theory] + [InlineData(".NETFramework,Version=v1.1", "net11")] + [InlineData(".NETFramework,Version=v2.0", "net20")] + [InlineData(".NETFramework,Version=v3.5", "net35")] + [InlineData(".NETFramework,Version=v4.0", "net40")] + [InlineData(".NETFramework,Version=v4.0.3", "net403")] + [InlineData(".NETFramework,Version=v4.5.1", "net451")] + [InlineData(".NETFramework,Version=v4.7.2", "net472")] + [InlineData(".NETFramework,Version=v4.8", "net48")] + [InlineData(".NETFramework,Version=v4.8.1", "net481")] + public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETFramework(string frameworkName, string expectedTfm) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Equal(expectedTfm, result); - - TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); - } - - [Theory] - [InlineData(".NETStandard,Version=v1.0", "netstandard1.0")] - [InlineData(".NETStandard,Version=v1.6", "netstandard1.6")] - [InlineData(".NETStandard,Version=v2.0", "netstandard2.0")] - [InlineData(".NETStandard,Version=v2.1", "netstandard2.1")] - public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETStandard(string frameworkName, string expectedTfm) - { - // Act + + // Assert + Assert.Equal(expectedTfm, result); + + TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); + } + + [Theory] + [InlineData(".NETStandard,Version=v1.0", "netstandard1.0")] + [InlineData(".NETStandard,Version=v1.6", "netstandard1.6")] + [InlineData(".NETStandard,Version=v2.0", "netstandard2.0")] + [InlineData(".NETStandard,Version=v2.1", "netstandard2.1")] + public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETStandard(string frameworkName, string expectedTfm) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Equal(expectedTfm, result); - - TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); - } - - [Theory] - [InlineData(".NETCoreApp,Version=v1.0", "netcoreapp1.0")] - [InlineData(".NETCoreApp,Version=v2.1", "netcoreapp2.1")] - [InlineData(".NETCoreApp,Version=v3.1", "netcoreapp3.1")] - public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETCoreApp(string frameworkName, string expectedTfm) - { - // Act + + // Assert + Assert.Equal(expectedTfm, result); + + TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); + } + + [Theory] + [InlineData(".NETCoreApp,Version=v1.0", "netcoreapp1.0")] + [InlineData(".NETCoreApp,Version=v2.1", "netcoreapp2.1")] + [InlineData(".NETCoreApp,Version=v3.1", "netcoreapp3.1")] + public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForNETCoreApp(string frameworkName, string expectedTfm) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Equal(expectedTfm, result); - - TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); - } - - [Theory] - [InlineData(".NETCoreApp,Version=v5.0", "net5.0")] - [InlineData(".NETCoreApp,Version=v6.0", "net6.0")] - [InlineData(".NETCoreApp,Version=v8.0", "net8.0")] - [InlineData(".NETCoreApp,Version=v9.0", "net9.0")] - [InlineData(".NETCoreApp,Version=v10.0", "net10.0")] - public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForModernNet(string frameworkName, string expectedTfm) - { - // Act + + // Assert + Assert.Equal(expectedTfm, result); + + TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); + } + + [Theory] + [InlineData(".NETCoreApp,Version=v5.0", "net5.0")] + [InlineData(".NETCoreApp,Version=v6.0", "net6.0")] + [InlineData(".NETCoreApp,Version=v8.0", "net8.0")] + [InlineData(".NETCoreApp,Version=v9.0", "net9.0")] + [InlineData(".NETCoreApp,Version=v10.0", "net10.0")] + public void ParseTargetFrameworkMoniker_ShouldReturnCorrectTfm_ForModernNet(string frameworkName, string expectedTfm) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Equal(expectedTfm, result); - - TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - public void ParseTargetFrameworkMoniker_ShouldReturnNull_WhenFrameworkNameIsNullOrEmpty(string frameworkName) - { - // Act + + // Assert + Assert.Equal(expectedTfm, result); + + TestOutput.WriteLine($"FrameworkName: {frameworkName} → TFM: {result}"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void ParseTargetFrameworkMoniker_ShouldReturnNull_WhenFrameworkNameIsNullOrEmpty(string frameworkName) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Null(result); - } - - [Theory] - [InlineData("not-a-framework-name")] - [InlineData(".NETSomethingUnknown,Version=v1.0")] - public void ParseTargetFrameworkMoniker_ShouldReturnNull_WhenFrameworkNameIsUnrecognised(string frameworkName) - { - // Act + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("not-a-framework-name")] + [InlineData(".NETSomethingUnknown,Version=v1.0")] + public void ParseTargetFrameworkMoniker_ShouldReturnNull_WhenFrameworkNameIsUnrecognised(string frameworkName) + { + // Act var result = ParseTargetFrameworkMoniker(frameworkName); - - // Assert - Assert.Null(result); - + + // Assert + Assert.Null(result); + TestOutput.WriteLine($"Unrecognised framework: {frameworkName} → null"); } diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs index dd9e49a..0d8b94c 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs @@ -1,1001 +1,1001 @@ -using BenchmarkDotNet.Configs; -using Codebelt.Extensions.Xunit; -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using Cuemon; -using Cuemon.Reflection; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -public class BenchmarkWorkspaceTest : Test -{ - private static readonly bool IsDebugBuild = GetBuildConfiguration(); - - public BenchmarkWorkspaceTest(ITestOutputHelper output) : base(output) - { - } - - private static bool GetBuildConfiguration() - { - return Decorator.Enclose(typeof(BenchmarkWorkspaceTest).Assembly).IsDebugBuild(); - } - - [Fact] - public void Constructor_ShouldThrowArgumentNullException_WhenOptionsIsNull() - { - // Arrange - BenchmarkWorkspaceOptions options = null; - - // Act & Assert - Assert.Throws(() => new BenchmarkWorkspace(options)); - } - - [Fact] - public void Constructor_ShouldThrowArgumentException_WhenOptionsAreInvalid() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = null - }; - - // Act & Assert - Assert.Throws(() => new BenchmarkWorkspace(options)); - } - - [Fact] - public void Constructor_ShouldSucceed_WhenOptionsAreValid() - { - // Arrange - var options = new BenchmarkWorkspaceOptions(); - - // Act - var workspace = new BenchmarkWorkspace(options); - - // Assert - Assert.NotNull(workspace); - } - - [Fact] - public void Constructor_ShouldEnableDisableOptimizationsValidator_WhenUseDebugBuildIsTrue() - { - // Arrange - var config = ManualConfig.CreateEmpty(); - var options = new BenchmarkWorkspaceOptions - { - Configuration = config, - AllowDebugBuild = true - }; - - // Act - var workspace = new BenchmarkWorkspace(options); - - // Assert - Assert.NotNull(workspace); - var manualConfig = options.Configuration as ManualConfig; - Assert.NotNull(manualConfig); - Assert.True(manualConfig.Options.HasFlag(ConfigOptions.DisableOptimizationsValidator)); - } - - [Fact] - public void Constructor_ShouldNotModifyConfiguration_WhenUseDebugBuildIsFalse() - { - // Arrange - var config = ManualConfig.CreateEmpty(); - var originalOptions = config.Options; - var options = new BenchmarkWorkspaceOptions - { - Configuration = config, - AllowDebugBuild = false - }; - - // Act - var workspace = new BenchmarkWorkspace(options); - - // Assert - Assert.NotNull(workspace); - var manualConfig = options.Configuration as ManualConfig; - Assert.NotNull(manualConfig); - Assert.Equal(originalOptions, manualConfig.Options); - } - - [Fact] - public void Constructor_ShouldNotModifyConfiguration_WhenConfigurationIsNotManualConfig() - { - // Arrange - var config = DefaultConfig.Instance; - var options = new BenchmarkWorkspaceOptions - { - Configuration = config - }; - - // Act - var workspace = new BenchmarkWorkspace(options); - - // Assert - Assert.NotNull(workspace); - Assert.Same(config, options.Configuration); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldThrowInvalidOperationException_WhenNoAssembliesFound() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - RepositoryTuningFolder = "tuning", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act & Assert - var exception = Assert.Throws(() => workspace.LoadBenchmarkAssemblies()); - - TestOutput.WriteLine($"{exception.Message}"); - - Assert.Contains("No assemblies were loaded", exception.Message); - Assert.Contains(IsDebugBuild ? "Debug" : "Release", exception.Message); - Assert.Contains("net10.0", exception.Message); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldCreateTuningDirectory_WhenItDoesNotExist() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - RepositoryTuningFolder = "tuning", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - - var workspace = new BenchmarkWorkspace(options); - var expectedTuningPath = Path.Combine(tempPath, "tuning"); - - // Act - try - { - workspace.LoadBenchmarkAssemblies(); - } - catch (InvalidOperationException) - { - // Expected when no assemblies found - } - - // Assert - Assert.True(Directory.Exists(expectedTuningPath)); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldReturnLoadedAssemblies_WhenMatchingAssembliesExist() - { - // Arrange - var options = new BenchmarkWorkspaceOptions() - { - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.NotEmpty(assemblies); - Assert.All(assemblies, assembly => Assert.NotNull(assembly)); - - TestOutput.WriteLine($"Current build configuration: {(IsDebugBuild ? "Debug" : "Release")}"); - TestOutput.WriteLine($"Loaded {assemblies.Length} benchmark assemblies:"); - foreach (var assembly in assemblies) - { - TestOutput.WriteLine($" - {assembly.GetName().Name}"); - } - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldFilterByBuildConfiguration() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - var expectedBuildConfig = IsDebugBuild ? "Debug" : "Release"; - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.All(assemblies, assembly => - { - var location = assembly.Location; - TestOutput.WriteLine($"Assembly location: {location}"); - Assert.Contains(expectedBuildConfig, location, StringComparison.OrdinalIgnoreCase); - }); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldFilterByTargetFrameworkMoniker() - { - // Arrange - var options = new BenchmarkWorkspaceOptions() - { - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - var expectedTfm = options.TargetFrameworkMoniker; - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.All(assemblies, assembly => - { - var location = assembly.Location; - TestOutput.WriteLine($"Assembly location: {location}"); - Assert.Contains(expectedTfm, location, StringComparison.OrdinalIgnoreCase); - }); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldFilterByBenchmarkProjectSuffix() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - BenchmarkProjectSuffix = "Benchmarks", - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.All(assemblies, assembly => - { - var name = assembly.GetName().Name; - TestOutput.WriteLine($"Assembly name: {name}"); - Assert.Contains("Benchmarks", name, StringComparison.OrdinalIgnoreCase); - }); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldNotLoadDuplicateAssemblies() - { - // Arrange - var options = new BenchmarkWorkspaceOptions() - { - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - var uniqueAssemblies = assemblies.Distinct().ToArray(); - Assert.Equal(assemblies.Length, uniqueAssemblies.Length); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldReuseAlreadyLoadedAssemblies() - { - // Arrange - var options = new BenchmarkWorkspaceOptions() - { - AllowDebugBuild = IsDebugBuild, - TargetFrameworkMoniker = "net10.0" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - var firstLoad = workspace.LoadBenchmarkAssemblies(); - var secondLoad = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(firstLoad); - Assert.NotNull(secondLoad); - Assert.All(firstLoad, firstAssembly => - { - var matchingSecondAssembly = secondLoad.FirstOrDefault(second => - AssemblyName.ReferenceMatchesDefinition(second.GetName(), firstAssembly.GetName())); - Assert.NotNull(matchingSecondAssembly); - }); - } - - [Fact] - public void PostProcessArtifacts_ShouldMoveFilesFromResultsDirectory() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - - Directory.CreateDirectory(resultsDir); - - var testFile1 = Path.Combine(resultsDir, "test1.txt"); - var testFile2 = Path.Combine(resultsDir, "test2.md"); - File.WriteAllText(testFile1, "Test content 1"); - File.WriteAllText(testFile2, "Test content 2"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act - workspace.PostProcessArtifacts(); - - // Assert - Assert.False(Directory.Exists(resultsDir)); - Assert.True(Directory.Exists(tuningDir)); - Assert.True(File.Exists(Path.Combine(tuningDir, "test1.txt"))); - Assert.True(File.Exists(Path.Combine(tuningDir, "test2.md"))); - - TestOutput.WriteLine($"Files moved successfully to: {tuningDir}"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void PostProcessArtifacts_ShouldDoNothing_WhenResultsDirectoryDoesNotExist() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - Directory.CreateDirectory(artifactsPath); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act - var exception = Record.Exception(() => workspace.PostProcessArtifacts()); - - // Assert - Assert.Null(exception); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void PostProcessArtifacts_ShouldOverwriteExistingFiles() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - - Directory.CreateDirectory(resultsDir); - Directory.CreateDirectory(tuningDir); - - var sourceFile = Path.Combine(resultsDir, "test.txt"); - var targetFile = Path.Combine(tuningDir, "test.txt"); - - File.WriteAllText(sourceFile, "New content"); - File.WriteAllText(targetFile, "Old content"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act - workspace.PostProcessArtifacts(); - - // Assert - Assert.False(Directory.Exists(resultsDir)); - Assert.True(File.Exists(targetFile)); - var content = File.ReadAllText(targetFile); - Assert.Equal("New content", content); - - TestOutput.WriteLine($"File successfully overwritten at: {targetFile}"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void PostProcessArtifacts_ShouldDeleteResultsDirectoryRecursively() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - var subDir = Path.Combine(resultsDir, "subdir"); - - Directory.CreateDirectory(subDir); - File.WriteAllText(Path.Combine(resultsDir, "file1.txt"), "Content 1"); - File.WriteAllText(Path.Combine(subDir, "file2.txt"), "Content 2"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act - workspace.PostProcessArtifacts(); - - // Assert - Assert.False(Directory.Exists(resultsDir)); - Assert.False(Directory.Exists(subDir)); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void PostProcessArtifacts_ShouldCreateTuningDirectory_WhenItDoesNotExist() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - - Directory.CreateDirectory(resultsDir); - File.WriteAllText(Path.Combine(resultsDir, "test.txt"), "Content"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - var workspace = new BenchmarkWorkspace(options); - var expectedTuningPath = Path.Combine(artifactsPath, "tuning"); - - // Act - workspace.PostProcessArtifacts(); - - // Assert - Assert.True(Directory.Exists(expectedTuningPath)); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void BenchmarkWorkspace_ShouldImplementIBenchmarkWorkspace() - { - // Arrange & Act - var options = new BenchmarkWorkspaceOptions(); - var workspace = new BenchmarkWorkspace(options); - - // Assert - Assert.IsAssignableFrom(workspace); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldHandleAssemblyLoadFailuresGracefully() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - var buildConfig = IsDebugBuild ? "Debug" : "Release"; - try - { - Directory.CreateDirectory(tempPath); - var tuningDir = Path.Combine(tempPath, "tuning"); - var buildDir = Path.Combine(tuningDir, "bin", buildConfig, "net10.0"); - Directory.CreateDirectory(buildDir); - - // Create an invalid DLL file - var invalidDll = Path.Combine(buildDir, "Invalid.Benchmarks.dll"); - File.WriteAllText(invalidDll, "This is not a valid assembly"); - - // Create a valid assembly reference - var validDll = Path.Combine(buildDir, "Valid.Benchmarks.dll"); - File.Copy(Assembly.GetExecutingAssembly().Location, validDll, true); - - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - RepositoryTuningFolder = "tuning", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - - var workspace = new BenchmarkWorkspace(options); - - // Act - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert - Assert.NotNull(assemblies); - Assert.NotEmpty(assemblies); - Assert.All(assemblies, assembly => Assert.NotNull(assembly)); - - TestOutput.WriteLine($"Build configuration: {buildConfig}"); - TestOutput.WriteLine($"Successfully loaded {assemblies.Length} valid assemblies while skipping invalid ones"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void GetReportsResultsPath_ShouldReturnCorrectPath_WhenOptionsAreValid() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config - }; - - // Act - var resultsPath = BenchmarkWorkspace.GetReportsResultsPath(options); - - // Assert - var expectedPath = Path.Combine(artifactsPath, "results"); - Assert.Equal(expectedPath, resultsPath); - - TestOutput.WriteLine($"Results path: {resultsPath}"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void GetReportsResultsPath_ShouldThrowArgumentException_WhenOptionsAreInvalid() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = null - }; - - // Act & Assert - Assert.Throws(() => BenchmarkWorkspace.GetReportsResultsPath(options)); - } - - [Fact] - public void GetReportsResultsPath_ShouldThrowArgumentNullException_WhenOptionsIsNull() - { - // Arrange - BenchmarkWorkspaceOptions options = null; - - // Act & Assert - Assert.Throws(() => BenchmarkWorkspace.GetReportsResultsPath(options)); - } - - [Fact] - public void GetReportsTuningPath_ShouldReturnCorrectPath_WhenOptionsAreValid() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - - // Act - var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); - - // Assert - var expectedPath = Path.Combine(artifactsPath, "tuning"); - Assert.Equal(expectedPath, tuningPath); - - TestOutput.WriteLine($"Tuning path: {tuningPath}"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void GetReportsTuningPath_ShouldThrowArgumentException_WhenOptionsAreInvalid() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = null - }; - - // Act & Assert - Assert.Throws(() => BenchmarkWorkspace.GetReportsTuningPath(options)); - } - - [Fact] - public void GetReportsTuningPath_ShouldThrowArgumentNullException_WhenOptionsIsNull() - { - // Arrange - BenchmarkWorkspaceOptions options = null; - - // Act & Assert - Assert.Throws(() => BenchmarkWorkspace.GetReportsTuningPath(options)); - } - - [Fact] - public void GetReportsTuningPath_ShouldUseCustomTuningFolder_WhenSpecified() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var customTuningFolder = "custom-tuning"; - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = customTuningFolder - }; - - // Act - var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); - - // Assert - var expectedPath = Path.Combine(artifactsPath, customTuningFolder); - Assert.Equal(expectedPath, tuningPath); - Assert.Contains(customTuningFolder, tuningPath); - - TestOutput.WriteLine($"Custom tuning path: {tuningPath}"); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void GetReportsResultsPath_AndGetReportsTuningPath_ShouldReturnDifferentPaths() - { - // Arrange - var options = new BenchmarkWorkspaceOptions - { - AllowDebugBuild = IsDebugBuild - }; - - // Act - var resultsPath = BenchmarkWorkspace.GetReportsResultsPath(options); - var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); - - // Assert - Assert.NotEqual(resultsPath, tuningPath); - Assert.Contains("results", resultsPath); - Assert.Contains("tuning", tuningPath); - - TestOutput.WriteLine($"Results path: {resultsPath}"); - TestOutput.WriteLine($"Tuning path: {tuningPath}"); - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldSkipEmptyFileName_WhenDllHasNoStem() - { - // Arrange – place a file whose stem is empty so that Path.GetFileNameWithoutExtension returns "" - // which exercises the IsNullOrEmpty(simpleName) guard in UpdateAssemblyLookup (lines 169-170). - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - var buildConfig = IsDebugBuild ? "Debug" : "Release"; - try - { - Directory.CreateDirectory(tempPath); - var tuningDir = Path.Combine(tempPath, "tuning"); - var buildDir = Path.Combine(tuningDir, "bin", buildConfig, "net10.0"); - Directory.CreateDirectory(buildDir); - - // A file named ".dll" has an empty stem – Path.GetFileNameWithoutExtension(".dll") == "" - File.WriteAllText(Path.Combine(buildDir, ".dll"), "not-an-assembly"); - - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - RepositoryTuningFolder = "tuning", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - var workspace = new BenchmarkWorkspace(options); - - // Act – will throw because there are no real assemblies; that's expected - var exception = Record.Exception(() => workspace.LoadBenchmarkAssemblies()); - - // Assert – the important thing is that no unhandled exception from UpdateAssemblyLookup occurred - Assert.IsType(exception); - Assert.Contains("No assemblies were loaded", exception.Message); - - TestOutput.WriteLine("Empty-stem .dll file was silently skipped as expected."); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void PostProcessArtifacts_ShouldSkipBenchmarkRunFiles_WhenResultsDirContainsThem() - { - // Arrange - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(tempPath); - var artifactsPath = Path.Combine(tempPath, "artifacts"); - var resultsDir = Path.Combine(artifactsPath, "results"); - var tuningDir = Path.Combine(artifactsPath, "tuning"); - - Directory.CreateDirectory(resultsDir); - - // BenchmarkRun-prefixed files should NOT be moved - File.WriteAllText(Path.Combine(resultsDir, "BenchmarkRun-20240101-120000.log"), "run log"); - // Regular report files SHOULD be moved - File.WriteAllText(Path.Combine(resultsDir, "MyBenchmark-report.md"), "report"); - - var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - Configuration = config, - RepositoryTuningFolder = "tuning" - }; - var workspace = new BenchmarkWorkspace(options); - - // Act - workspace.PostProcessArtifacts(); - - // Assert – the report file was moved but the BenchmarkRun log was NOT - Assert.False(Directory.Exists(resultsDir), "results directory should be deleted"); - Assert.True(File.Exists(Path.Combine(tuningDir, "MyBenchmark-report.md"))); - Assert.False(File.Exists(Path.Combine(tuningDir, "BenchmarkRun-20240101-120000.log"))); - - TestOutput.WriteLine("BenchmarkRun files correctly excluded from PostProcessArtifacts move."); - } - finally - { - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } - } - } - - [Fact] - public void LoadBenchmarkAssemblies_ShouldSkipDuplicateAssembly_WhenSameIdentityAppearsAtTwoPaths() - { - // This test exercises lines 141-142 (the duplicate-assembly-in-queue guard): - // - // if (assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName))) - // { - // continue; // ← lines 141-142 - // } - // - // The guard fires when: - // 1. 'alreadyLoaded' (snapshot taken BEFORE the loop) does NOT contain the assembly, - // 2. the loop adds path1 of the assembly to 'assemblies', - // 3. the loop then encounters path2 with the SAME identity – - // 'alreadyLoaded' still has no entry (same snapshot), so the first `if` is skipped, - // but `assemblies.Any(...)` is TRUE → duplicate guard fires. - // - // To guarantee the assembly is never in the initial AppDomain snapshot we generate a - // brand-new assembly with a unique GUID-based name via PersistedAssemblyBuilder. - - var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - var buildConfig = IsDebugBuild ? "Debug" : "Release"; - try - { - // Create two subdirectories that both match the build segment - var proj1Dir = Path.Combine(tempPath, "tuning", "proj1", "bin", buildConfig, "net10.0"); - var proj2Dir = Path.Combine(tempPath, "tuning", "proj2", "bin", buildConfig, "net10.0"); - Directory.CreateDirectory(proj1Dir); - Directory.CreateDirectory(proj2Dir); - - // Build a minimal in-memory assembly with a guaranteed-unique name - var uniqueId = Guid.NewGuid().ToString("N"); - var asmName = new AssemblyName($"Unique{uniqueId}.Benchmarks") { Version = new Version(1, 0, 0, 0) }; - - var persistedBuilder = new System.Reflection.Emit.PersistedAssemblyBuilder(asmName, typeof(object).Assembly); - persistedBuilder.DefineDynamicModule("main") - .DefineType("Placeholder", System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class) - .CreateType(); - - // Save to path1, then copy to path2 so both carry the SAME assembly identity - var fileName = $"Unique{uniqueId}.Benchmarks.dll"; - var path1 = Path.Combine(proj1Dir, fileName); - var path2 = Path.Combine(proj2Dir, fileName); - persistedBuilder.Save(path1); - File.Copy(path1, path2); - - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = tempPath, - RepositoryTuningFolder = "tuning", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = IsDebugBuild - }; - var workspace = new BenchmarkWorkspace(options); - - // Act – both paths match, one is loaded, the second hits the duplicate guard - var assemblies = workspace.LoadBenchmarkAssemblies(); - - // Assert – only ONE assembly loaded despite two matching paths - Assert.NotNull(assemblies); - Assert.Equal(1, assemblies.Length); - Assert.Contains(assemblies, a => a.GetName().Name == asmName.Name); - - TestOutput.WriteLine($"Loaded {assemblies.Length} assembly (duplicate suppressed): {assemblies[0].GetName().Name}"); - } - finally - { - // Unload assemblies and release file locks before cleanup on Windows - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - - // On Windows, loaded DLLs may still hold locks; retry deletion with delay - if (Directory.Exists(tempPath)) - { - const int maxRetries = 5; - const int delayMs = 200; - var lastException = (Exception)null; - for (int i = 0; i < maxRetries; i++) - { - try - { - Directory.Delete(tempPath, true); - break; - } - catch (UnauthorizedAccessException ex) - { - lastException = ex; - if (i < maxRetries - 1) - { - System.Threading.Thread.Sleep(delayMs); - } - } - } - // If we still can't delete after retries, log but don't fail the test - // The OS will clean up the temp directory eventually - if (Directory.Exists(tempPath)) - { - TestOutput.WriteLine($"Warning: Could not delete temp directory {tempPath}"); - } - } - } - } -} +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.Xunit; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Cuemon; +using Cuemon.Reflection; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +public class BenchmarkWorkspaceTest : Test +{ + private static readonly bool IsDebugBuild = GetBuildConfiguration(); + + public BenchmarkWorkspaceTest(ITestOutputHelper output) : base(output) + { + } + + private static bool GetBuildConfiguration() + { + return Decorator.Enclose(typeof(BenchmarkWorkspaceTest).Assembly).IsDebugBuild(); + } + + [Fact] + public void Constructor_ShouldThrowArgumentNullException_WhenOptionsIsNull() + { + // Arrange + BenchmarkWorkspaceOptions options = null; + + // Act & Assert + Assert.Throws(() => new BenchmarkWorkspace(options)); + } + + [Fact] + public void Constructor_ShouldThrowArgumentException_WhenOptionsAreInvalid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = null + }; + + // Act & Assert + Assert.Throws(() => new BenchmarkWorkspace(options)); + } + + [Fact] + public void Constructor_ShouldSucceed_WhenOptionsAreValid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + } + + [Fact] + public void Constructor_ShouldEnableDisableOptimizationsValidator_WhenUseDebugBuildIsTrue() + { + // Arrange + var config = ManualConfig.CreateEmpty(); + var options = new BenchmarkWorkspaceOptions + { + Configuration = config, + AllowDebugBuild = true + }; + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + var manualConfig = options.Configuration as ManualConfig; + Assert.NotNull(manualConfig); + Assert.True(manualConfig.Options.HasFlag(ConfigOptions.DisableOptimizationsValidator)); + } + + [Fact] + public void Constructor_ShouldNotModifyConfiguration_WhenUseDebugBuildIsFalse() + { + // Arrange + var config = ManualConfig.CreateEmpty(); + var originalOptions = config.Options; + var options = new BenchmarkWorkspaceOptions + { + Configuration = config, + AllowDebugBuild = false + }; + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + var manualConfig = options.Configuration as ManualConfig; + Assert.NotNull(manualConfig); + Assert.Equal(originalOptions, manualConfig.Options); + } + + [Fact] + public void Constructor_ShouldNotModifyConfiguration_WhenConfigurationIsNotManualConfig() + { + // Arrange + var config = DefaultConfig.Instance; + var options = new BenchmarkWorkspaceOptions + { + Configuration = config + }; + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + Assert.Same(config, options.Configuration); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldThrowInvalidOperationException_WhenNoAssembliesFound() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act & Assert + var exception = Assert.Throws(() => workspace.LoadBenchmarkAssemblies()); + + TestOutput.WriteLine($"{exception.Message}"); + + Assert.Contains("No assemblies were loaded", exception.Message); + Assert.Contains(IsDebugBuild ? "Debug" : "Release", exception.Message); + Assert.Contains("net10.0", exception.Message); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldCreateTuningDirectory_WhenItDoesNotExist() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + + var workspace = new BenchmarkWorkspace(options); + var expectedTuningPath = Path.Combine(tempPath, "tuning"); + + // Act + try + { + workspace.LoadBenchmarkAssemblies(); + } + catch (InvalidOperationException) + { + // Expected when no assemblies found + } + + // Assert + Assert.True(Directory.Exists(expectedTuningPath)); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldReturnLoadedAssemblies_WhenMatchingAssembliesExist() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.NotEmpty(assemblies); + Assert.All(assemblies, assembly => Assert.NotNull(assembly)); + + TestOutput.WriteLine($"Current build configuration: {(IsDebugBuild ? "Debug" : "Release")}"); + TestOutput.WriteLine($"Loaded {assemblies.Length} benchmark assemblies:"); + foreach (var assembly in assemblies) + { + TestOutput.WriteLine($" - {assembly.GetName().Name}"); + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldFilterByBuildConfiguration() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + var expectedBuildConfig = IsDebugBuild ? "Debug" : "Release"; + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.All(assemblies, assembly => + { + var location = assembly.Location; + TestOutput.WriteLine($"Assembly location: {location}"); + Assert.Contains(expectedBuildConfig, location, StringComparison.OrdinalIgnoreCase); + }); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldFilterByTargetFrameworkMoniker() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + var expectedTfm = options.TargetFrameworkMoniker; + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.All(assemblies, assembly => + { + var location = assembly.Location; + TestOutput.WriteLine($"Assembly location: {location}"); + Assert.Contains(expectedTfm, location, StringComparison.OrdinalIgnoreCase); + }); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldFilterByBenchmarkProjectSuffix() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = "Benchmarks", + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.All(assemblies, assembly => + { + var name = assembly.GetName().Name; + TestOutput.WriteLine($"Assembly name: {name}"); + Assert.Contains("Benchmarks", name, StringComparison.OrdinalIgnoreCase); + }); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldNotLoadDuplicateAssemblies() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + var uniqueAssemblies = assemblies.Distinct().ToArray(); + Assert.Equal(assemblies.Length, uniqueAssemblies.Length); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldReuseAlreadyLoadedAssemblies() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var firstLoad = workspace.LoadBenchmarkAssemblies(); + var secondLoad = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(firstLoad); + Assert.NotNull(secondLoad); + Assert.All(firstLoad, firstAssembly => + { + var matchingSecondAssembly = secondLoad.FirstOrDefault(second => + AssemblyName.ReferenceMatchesDefinition(second.GetName(), firstAssembly.GetName())); + Assert.NotNull(matchingSecondAssembly); + }); + } + + [Fact] + public void PostProcessArtifacts_ShouldMoveFilesFromResultsDirectory() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + + Directory.CreateDirectory(resultsDir); + + var testFile1 = Path.Combine(resultsDir, "test1.txt"); + var testFile2 = Path.Combine(resultsDir, "test2.md"); + File.WriteAllText(testFile1, "Test content 1"); + File.WriteAllText(testFile2, "Test content 2"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.True(Directory.Exists(tuningDir)); + Assert.True(File.Exists(Path.Combine(tuningDir, "test1.txt"))); + Assert.True(File.Exists(Path.Combine(tuningDir, "test2.md"))); + + TestOutput.WriteLine($"Files moved successfully to: {tuningDir}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldDoNothing_WhenResultsDirectoryDoesNotExist() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + Directory.CreateDirectory(artifactsPath); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + var exception = Record.Exception(() => workspace.PostProcessArtifacts()); + + // Assert + Assert.Null(exception); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldOverwriteExistingFiles() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + + Directory.CreateDirectory(resultsDir); + Directory.CreateDirectory(tuningDir); + + var sourceFile = Path.Combine(resultsDir, "test.txt"); + var targetFile = Path.Combine(tuningDir, "test.txt"); + + File.WriteAllText(sourceFile, "New content"); + File.WriteAllText(targetFile, "Old content"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.True(File.Exists(targetFile)); + var content = File.ReadAllText(targetFile); + Assert.Equal("New content", content); + + TestOutput.WriteLine($"File successfully overwritten at: {targetFile}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldDeleteResultsDirectoryRecursively() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var subDir = Path.Combine(resultsDir, "subdir"); + + Directory.CreateDirectory(subDir); + File.WriteAllText(Path.Combine(resultsDir, "file1.txt"), "Content 1"); + File.WriteAllText(Path.Combine(subDir, "file2.txt"), "Content 2"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.False(Directory.Exists(subDir)); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldCreateTuningDirectory_WhenItDoesNotExist() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + + Directory.CreateDirectory(resultsDir); + File.WriteAllText(Path.Combine(resultsDir, "test.txt"), "Content"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + var expectedTuningPath = Path.Combine(artifactsPath, "tuning"); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.True(Directory.Exists(expectedTuningPath)); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void BenchmarkWorkspace_ShouldImplementIBenchmarkWorkspace() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.IsAssignableFrom(workspace); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldHandleAssemblyLoadFailuresGracefully() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var buildConfig = IsDebugBuild ? "Debug" : "Release"; + try + { + Directory.CreateDirectory(tempPath); + var tuningDir = Path.Combine(tempPath, "tuning"); + var buildDir = Path.Combine(tuningDir, "bin", buildConfig, "net10.0"); + Directory.CreateDirectory(buildDir); + + // Create an invalid DLL file + var invalidDll = Path.Combine(buildDir, "Invalid.Benchmarks.dll"); + File.WriteAllText(invalidDll, "This is not a valid assembly"); + + // Create a valid assembly reference + var validDll = Path.Combine(buildDir, "Valid.Benchmarks.dll"); + File.Copy(Assembly.GetExecutingAssembly().Location, validDll, true); + + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.NotEmpty(assemblies); + Assert.All(assemblies, assembly => Assert.NotNull(assembly)); + + TestOutput.WriteLine($"Build configuration: {buildConfig}"); + TestOutput.WriteLine($"Successfully loaded {assemblies.Length} valid assemblies while skipping invalid ones"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void GetReportsResultsPath_ShouldReturnCorrectPath_WhenOptionsAreValid() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config + }; + + // Act + var resultsPath = BenchmarkWorkspace.GetReportsResultsPath(options); + + // Assert + var expectedPath = Path.Combine(artifactsPath, "results"); + Assert.Equal(expectedPath, resultsPath); + + TestOutput.WriteLine($"Results path: {resultsPath}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void GetReportsResultsPath_ShouldThrowArgumentException_WhenOptionsAreInvalid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = null + }; + + // Act & Assert + Assert.Throws(() => BenchmarkWorkspace.GetReportsResultsPath(options)); + } + + [Fact] + public void GetReportsResultsPath_ShouldThrowArgumentNullException_WhenOptionsIsNull() + { + // Arrange + BenchmarkWorkspaceOptions options = null; + + // Act & Assert + Assert.Throws(() => BenchmarkWorkspace.GetReportsResultsPath(options)); + } + + [Fact] + public void GetReportsTuningPath_ShouldReturnCorrectPath_WhenOptionsAreValid() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + // Act + var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); + + // Assert + var expectedPath = Path.Combine(artifactsPath, "tuning"); + Assert.Equal(expectedPath, tuningPath); + + TestOutput.WriteLine($"Tuning path: {tuningPath}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void GetReportsTuningPath_ShouldThrowArgumentException_WhenOptionsAreInvalid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = null + }; + + // Act & Assert + Assert.Throws(() => BenchmarkWorkspace.GetReportsTuningPath(options)); + } + + [Fact] + public void GetReportsTuningPath_ShouldThrowArgumentNullException_WhenOptionsIsNull() + { + // Arrange + BenchmarkWorkspaceOptions options = null; + + // Act & Assert + Assert.Throws(() => BenchmarkWorkspace.GetReportsTuningPath(options)); + } + + [Fact] + public void GetReportsTuningPath_ShouldUseCustomTuningFolder_WhenSpecified() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var customTuningFolder = "custom-tuning"; + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = customTuningFolder + }; + + // Act + var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); + + // Assert + var expectedPath = Path.Combine(artifactsPath, customTuningFolder); + Assert.Equal(expectedPath, tuningPath); + Assert.Contains(customTuningFolder, tuningPath); + + TestOutput.WriteLine($"Custom tuning path: {tuningPath}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void GetReportsResultsPath_AndGetReportsTuningPath_ShouldReturnDifferentPaths() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + AllowDebugBuild = IsDebugBuild + }; + + // Act + var resultsPath = BenchmarkWorkspace.GetReportsResultsPath(options); + var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options); + + // Assert + Assert.NotEqual(resultsPath, tuningPath); + Assert.Contains("results", resultsPath); + Assert.Contains("tuning", tuningPath); + + TestOutput.WriteLine($"Results path: {resultsPath}"); + TestOutput.WriteLine($"Tuning path: {tuningPath}"); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldSkipEmptyFileName_WhenDllHasNoStem() + { + // Arrange – place a file whose stem is empty so that Path.GetFileNameWithoutExtension returns "" + // which exercises the IsNullOrEmpty(simpleName) guard in UpdateAssemblyLookup (lines 169-170). + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var buildConfig = IsDebugBuild ? "Debug" : "Release"; + try + { + Directory.CreateDirectory(tempPath); + var tuningDir = Path.Combine(tempPath, "tuning"); + var buildDir = Path.Combine(tuningDir, "bin", buildConfig, "net10.0"); + Directory.CreateDirectory(buildDir); + + // A file named ".dll" has an empty stem – Path.GetFileNameWithoutExtension(".dll") == "" + File.WriteAllText(Path.Combine(buildDir, ".dll"), "not-an-assembly"); + + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + var workspace = new BenchmarkWorkspace(options); + + // Act – will throw because there are no real assemblies; that's expected + var exception = Record.Exception(() => workspace.LoadBenchmarkAssemblies()); + + // Assert – the important thing is that no unhandled exception from UpdateAssemblyLookup occurred + Assert.IsType(exception); + Assert.Contains("No assemblies were loaded", exception.Message); + + TestOutput.WriteLine("Empty-stem .dll file was silently skipped as expected."); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldSkipBenchmarkRunFiles_WhenResultsDirContainsThem() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + + Directory.CreateDirectory(resultsDir); + + // BenchmarkRun-prefixed files should NOT be moved + File.WriteAllText(Path.Combine(resultsDir, "BenchmarkRun-20240101-120000.log"), "run log"); + // Regular report files SHOULD be moved + File.WriteAllText(Path.Combine(resultsDir, "MyBenchmark-report.md"), "report"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert – the report file was moved but the BenchmarkRun log was NOT + Assert.False(Directory.Exists(resultsDir), "results directory should be deleted"); + Assert.True(File.Exists(Path.Combine(tuningDir, "MyBenchmark-report.md"))); + Assert.False(File.Exists(Path.Combine(tuningDir, "BenchmarkRun-20240101-120000.log"))); + + TestOutput.WriteLine("BenchmarkRun files correctly excluded from PostProcessArtifacts move."); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldSkipDuplicateAssembly_WhenSameIdentityAppearsAtTwoPaths() + { + // This test exercises lines 141-142 (the duplicate-assembly-in-queue guard): + // + // if (assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName))) + // { + // continue; // ← lines 141-142 + // } + // + // The guard fires when: + // 1. 'alreadyLoaded' (snapshot taken BEFORE the loop) does NOT contain the assembly, + // 2. the loop adds path1 of the assembly to 'assemblies', + // 3. the loop then encounters path2 with the SAME identity – + // 'alreadyLoaded' still has no entry (same snapshot), so the first `if` is skipped, + // but `assemblies.Any(...)` is TRUE → duplicate guard fires. + // + // To guarantee the assembly is never in the initial AppDomain snapshot we generate a + // brand-new assembly with a unique GUID-based name via PersistedAssemblyBuilder. + + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var buildConfig = IsDebugBuild ? "Debug" : "Release"; + try + { + // Create two subdirectories that both match the build segment + var proj1Dir = Path.Combine(tempPath, "tuning", "proj1", "bin", buildConfig, "net10.0"); + var proj2Dir = Path.Combine(tempPath, "tuning", "proj2", "bin", buildConfig, "net10.0"); + Directory.CreateDirectory(proj1Dir); + Directory.CreateDirectory(proj2Dir); + + // Build a minimal in-memory assembly with a guaranteed-unique name + var uniqueId = Guid.NewGuid().ToString("N"); + var asmName = new AssemblyName($"Unique{uniqueId}.Benchmarks") { Version = new Version(1, 0, 0, 0) }; + + var persistedBuilder = new System.Reflection.Emit.PersistedAssemblyBuilder(asmName, typeof(object).Assembly); + persistedBuilder.DefineDynamicModule("main") + .DefineType("Placeholder", System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class) + .CreateType(); + + // Save to path1, then copy to path2 so both carry the SAME assembly identity + var fileName = $"Unique{uniqueId}.Benchmarks.dll"; + var path1 = Path.Combine(proj1Dir, fileName); + var path2 = Path.Combine(proj2Dir, fileName); + persistedBuilder.Save(path1); + File.Copy(path1, path2); + + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + var workspace = new BenchmarkWorkspace(options); + + // Act – both paths match, one is loaded, the second hits the duplicate guard + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert – only ONE assembly loaded despite two matching paths + Assert.NotNull(assemblies); + Assert.Equal(1, assemblies.Length); + Assert.Contains(assemblies, a => a.GetName().Name == asmName.Name); + + TestOutput.WriteLine($"Loaded {assemblies.Length} assembly (duplicate suppressed): {assemblies[0].GetName().Name}"); + } + finally + { + // Unload assemblies and release file locks before cleanup on Windows + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + // On Windows, loaded DLLs may still hold locks; retry deletion with delay + if (Directory.Exists(tempPath)) + { + const int maxRetries = 5; + const int delayMs = 200; + var lastException = (Exception)null; + for (int i = 0; i < maxRetries; i++) + { + try + { + Directory.Delete(tempPath, true); + break; + } + catch (UnauthorizedAccessException ex) + { + lastException = ex; + if (i < maxRetries - 1) + { + System.Threading.Thread.Sleep(delayMs); + } + } + } + // If we still can't delete after retries, log but don't fail the test + // The OS will clean up the temp directory eventually + if (Directory.Exists(tempPath)) + { + TestOutput.WriteLine($"Warning: Could not delete temp directory {tempPath}"); + } + } + } + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs index 5f490e4..697c70c 100644 --- a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs @@ -1,77 +1,76 @@ -using System; -using System.Reflection; -using Codebelt.Extensions.Xunit; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Codebelt.Extensions.BenchmarkDotNet -{ - public class ServiceCollectionExtensionsTest : Test - { - public ServiceCollectionExtensionsTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void AddBenchmarkWorkspace_ShouldThrowWhenServicesIsNull() - { - Assert.Throws(() => ServiceCollectionExtensions.AddBenchmarkWorkspace((IServiceCollection)null)); - } - - [Fact] - public void AddBenchmarkWorkspace_ShouldRegisterDefaultWorkspaceAndOptions_WhenCalledWithoutGeneric() - { - var services = new ServiceCollection(); - services.AddBenchmarkWorkspace(setup: options => options.BenchmarkProjectSuffix = "MySuffix"); - using var sp = services.BuildServiceProvider(); - - var workspace = sp.GetRequiredService(); - Assert.IsType(workspace); - - var options = sp.GetRequiredService(); - Assert.Equal("MySuffix", options.BenchmarkProjectSuffix); - } - - [Fact] - public void AddBenchmarkWorkspace_GenericOverload_ShouldRegisterCustomImplementationAndOptions() - { - var services = new ServiceCollection(); - services.AddBenchmarkWorkspace(setup: options => options.RepositoryPath = "repo-path"); - using var sp = services.BuildServiceProvider(); - - var workspace = sp.GetRequiredService(); - Assert.IsType(workspace); - - var options = sp.GetRequiredService(); - Assert.Equal("repo-path", options.RepositoryPath); - } - - [Fact] - public void AddBenchmarkWorkspace_GenericOverload_ShouldUseNullCoalescingLambda_WhenSetupIsNull() - { - // When setup is null, the Configure call registers a no-op lambda (_ => {}). - // Resolving IOptions.Value causes the Options framework to - // invoke all registered configure-actions, which executes the null-coalescing lambda body. - var services = new ServiceCollection(); - services.AddBenchmarkWorkspace(setup: null); - using var sp = services.BuildServiceProvider(); - - // Resolving via IOptions triggers the registered configure action (the no-op lambda) - var optionsAccessor = sp.GetRequiredService>(); - var options = optionsAccessor.Value; - - Assert.NotNull(options); - Assert.IsType(sp.GetRequiredService()); - - TestOutput.WriteLine($"BenchmarkProjectSuffix: {options.BenchmarkProjectSuffix}"); - } - - private sealed class FakeWorkspace : IBenchmarkWorkspace - { - public Assembly[] LoadBenchmarkAssemblies() => Array.Empty(); - - public void PostProcessArtifacts() { } - } - } -} +using System; +using System.Reflection; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +public class ServiceCollectionExtensionsTest : Test +{ + public ServiceCollectionExtensionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void AddBenchmarkWorkspace_ShouldThrowWhenServicesIsNull() + { + Assert.Throws(() => ServiceCollectionExtensions.AddBenchmarkWorkspace((IServiceCollection)null)); + } + + [Fact] + public void AddBenchmarkWorkspace_ShouldRegisterDefaultWorkspaceAndOptions_WhenCalledWithoutGeneric() + { + var services = new ServiceCollection(); + services.AddBenchmarkWorkspace(setup: options => options.BenchmarkProjectSuffix = "MySuffix"); + using var sp = services.BuildServiceProvider(); + + var workspace = sp.GetRequiredService(); + Assert.IsType(workspace); + + var options = sp.GetRequiredService(); + Assert.Equal("MySuffix", options.BenchmarkProjectSuffix); + } + + [Fact] + public void AddBenchmarkWorkspace_GenericOverload_ShouldRegisterCustomImplementationAndOptions() + { + var services = new ServiceCollection(); + services.AddBenchmarkWorkspace(setup: options => options.RepositoryPath = "repo-path"); + using var sp = services.BuildServiceProvider(); + + var workspace = sp.GetRequiredService(); + Assert.IsType(workspace); + + var options = sp.GetRequiredService(); + Assert.Equal("repo-path", options.RepositoryPath); + } + + [Fact] + public void AddBenchmarkWorkspace_GenericOverload_ShouldUseNullCoalescingLambda_WhenSetupIsNull() + { + // When setup is null, the Configure call registers a no-op lambda (_ => {}). + // Resolving IOptions.Value causes the Options framework to + // invoke all registered configure-actions, which executes the null-coalescing lambda body. + var services = new ServiceCollection(); + services.AddBenchmarkWorkspace(setup: null); + using var sp = services.BuildServiceProvider(); + + // Resolving via IOptions triggers the registered configure action (the no-op lambda) + var optionsAccessor = sp.GetRequiredService>(); + var options = optionsAccessor.Value; + + Assert.NotNull(options); + Assert.IsType(sp.GetRequiredService()); + + TestOutput.WriteLine($"BenchmarkProjectSuffix: {options.BenchmarkProjectSuffix}"); + } + + private sealed class FakeWorkspace : IBenchmarkWorkspace + { + public Assembly[] LoadBenchmarkAssemblies() => Array.Empty(); + + public void PostProcessArtifacts() { } + } +} diff --git a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs index d639e65..de62b42 100644 --- a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs +++ b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs @@ -1,25 +1,24 @@ -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Jobs; -using Codebelt.Extensions.BenchmarkDotNet.Console; - -namespace Codebelt.Extensions.BenchmarkDotNet.Runner -{ - public class Program - { - public static void Main(string[] args) - { - BenchmarkProgram.Run(args, o => - { - o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; - o.SkipBenchmarksWithReports = true; - o.ConfigureBenchmarkDotNet(c => - { - var slimJob = BenchmarkWorkspaceOptions.Slim; - return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) - .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); - }); - }); - } - } -} +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using Codebelt.Extensions.BenchmarkDotNet.Console; + +namespace Codebelt.Extensions.BenchmarkDotNet.Runner; + +public class Program +{ + public static void Main(string[] args) + { + BenchmarkProgram.Run(args, o => + { + o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; + o.SkipBenchmarksWithReports = true; + o.ConfigureBenchmarkDotNet(c => + { + var slimJob = BenchmarkWorkspaceOptions.Slim; + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)) + .AddJob(slimJob.WithRuntime(CoreRuntime.CreateForNewVersion("net11.0", ".NET 11.0"))); + }); + }); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs index 0371c76..e6bc104 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs @@ -1,72 +1,72 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using System; -using System.IO; -using System.Reflection; -using Cuemon; -using Cuemon.Reflection; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -[MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -public class BenchmarkWorkspaceBenchmark -{ - private BenchmarkWorkspace _workspace; - - [GlobalSetup] - public void GlobalSetup() - { - var isDebugBuild = Decorator.Enclose(GetType().Assembly).IsDebugBuild(); - var options = new BenchmarkWorkspaceOptions() - { - RepositoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()), - AllowDebugBuild = isDebugBuild - }; - _workspace = new BenchmarkWorkspace(options); - - var buildConfig = isDebugBuild - ? "Debug" - : "Release"; - var tuningDir = Path.Combine(options.RepositoryPath, "tuning"); - - CreateBenchmarkAssembly(tuningDir, buildConfig, "net10.0", "Valid.Benchmarks.dll"); - CreateBenchmarkAssembly(tuningDir, buildConfig, "net9.0", "Valid.Benchmarks.dll"); - } - - [Benchmark(Baseline = true, Description = "Construct BenchmarkDotNetWorkspace")] - public void ConstructWorkspaceBenchmark() - { - var v = new BenchmarkWorkspace(new BenchmarkWorkspaceOptions()); - } - - [Benchmark(Description = "Load assemblies from tuning folder (no matching assemblies)")] - public void LoadBenchmarkAssembliesBenchmark() - { - // In the prepared environment there are no matching *.Benchmarks.dll files, - // so LoadBenchmarkAssemblies will exercise the enumeration/path logic without loading assemblies. - var result = _workspace.LoadBenchmarkAssemblies(); - GC.KeepAlive(result); - } - - [Benchmark(Description = "PostProcessArtifacts (move results -> tuning folder)")] - public void PostProcessArtifactsBenchmark() - { - _workspace.PostProcessArtifacts(); - } - - /// - /// Creates a benchmark assembly by copying the executing assembly to a target framework-specific build directory. - /// - /// The base tuning directory path. - /// The build configuration (e.g., "Debug" or "Release"). - /// The target framework moniker (e.g., "net9.0", "net10.0"). - /// The name of the DLL file to create. - private void CreateBenchmarkAssembly(string tuningDir, string buildConfig, string targetFramework, string dllName) - { - var buildDir = Path.Combine(tuningDir, "bin", buildConfig, targetFramework); - Directory.CreateDirectory(buildDir); - var targetDll = Path.Combine(buildDir, dllName); - File.Copy(Assembly.GetExecutingAssembly().Location, targetDll, true); - } -} +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using System; +using System.IO; +using System.Reflection; +using Cuemon; +using Cuemon.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkWorkspaceBenchmark +{ + private BenchmarkWorkspace _workspace; + + [GlobalSetup] + public void GlobalSetup() + { + var isDebugBuild = Decorator.Enclose(GetType().Assembly).IsDebugBuild(); + var options = new BenchmarkWorkspaceOptions() + { + RepositoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()), + AllowDebugBuild = isDebugBuild + }; + _workspace = new BenchmarkWorkspace(options); + + var buildConfig = isDebugBuild + ? "Debug" + : "Release"; + var tuningDir = Path.Combine(options.RepositoryPath, "tuning"); + + CreateBenchmarkAssembly(tuningDir, buildConfig, "net10.0", "Valid.Benchmarks.dll"); + CreateBenchmarkAssembly(tuningDir, buildConfig, "net11.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., "net10.0", "net11.0"). + /// The name of the DLL file to create. + private void CreateBenchmarkAssembly(string tuningDir, string buildConfig, string targetFramework, string dllName) + { + var buildDir = Path.Combine(tuningDir, "bin", buildConfig, targetFramework); + Directory.CreateDirectory(buildDir); + var targetDll = Path.Combine(buildDir, dllName); + File.Copy(Assembly.GetExecutingAssembly().Location, targetDll, true); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs index 0006210..675506b 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs @@ -1,114 +1,114 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using System; - -namespace Codebelt.Extensions.BenchmarkDotNet; - -[MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -public class BenchmarkWorkspaceOptionsBenchmark -{ - private BenchmarkWorkspaceOptions _options; - - [GlobalSetup] - public void Setup() - { - // Pre-allocate an instance for benchmarks that need existing state - _options = new BenchmarkWorkspaceOptions(); - } - - [Benchmark(Baseline = true, Description = "Create default BenchmarkWorkspaceOptions")] - [BenchmarkCategory("Construction")] - public BenchmarkWorkspaceOptions CreateDefaultOptions() - { - return new BenchmarkWorkspaceOptions(); - } - - [Benchmark(Description = "Create and configure BenchmarkWorkspaceOptions")] - [BenchmarkCategory("Construction")] - public BenchmarkWorkspaceOptions CreateAndConfigureOptions() - { - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = AppContext.BaseDirectory, - RepositoryTuningFolder = "tuning", - RepositoryReportsFolder = "reports", - BenchmarkProjectSuffix = "Benchmarks", - TargetFrameworkMoniker = "net9.0", - AllowDebugBuild = false - }; - return options; - } - - [Benchmark(Description = "ValidateOptions - valid state")] - [BenchmarkCategory("Validation")] - public void ValidateOptions_ValidState() - { - _options.ValidateOptions(); - } - - [Benchmark(Description = "PostConfigureOptions - default config")] - [BenchmarkCategory("Configuration")] - public void PostConfigureOptions_DefaultConfig() - { - var options = new BenchmarkWorkspaceOptions(); - options.PostConfigureOptions(); - GC.KeepAlive(options); - } - - [Benchmark(Description = "PostConfigureOptions - custom config")] - [BenchmarkCategory("Configuration")] - public void PostConfigureOptions_CustomConfig() - { - var options = new BenchmarkWorkspaceOptions - { - Configuration = ManualConfig.CreateEmpty() - }; - options.PostConfigureOptions(); - GC.KeepAlive(options); - } - - [Benchmark(Description = "Property access - RepositoryPath")] - [BenchmarkCategory("PropertyAccess")] - public string AccessRepositoryPath() - { - return _options.RepositoryPath; - } - - [Benchmark(Description = "Property access - Configuration")] - [BenchmarkCategory("PropertyAccess")] - public IConfig AccessConfiguration() - { - return _options.Configuration; - } - - [Benchmark(Description = "Property modification - set all properties")] - [BenchmarkCategory("PropertyModification")] - public void SetAllProperties() - { - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = AppContext.BaseDirectory, - RepositoryTuningFolder = "custom-tuning", - RepositoryReportsFolder = "custom-reports", - BenchmarkProjectSuffix = "Perf", - TargetFrameworkMoniker = "net10.0", - AllowDebugBuild = true - }; - GC.KeepAlive(options); - } - - [Benchmark(Description = "Full lifecycle - create, configure, validate")] - [BenchmarkCategory("Lifecycle")] - public void FullLifecycle() - { - var options = new BenchmarkWorkspaceOptions - { - RepositoryPath = AppContext.BaseDirectory, - TargetFrameworkMoniker = "net9.0" - }; - options.PostConfigureOptions(); - options.ValidateOptions(); - GC.KeepAlive(options); - } -} +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using System; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkWorkspaceOptionsBenchmark +{ + private BenchmarkWorkspaceOptions _options; + + [GlobalSetup] + public void Setup() + { + // Pre-allocate an instance for benchmarks that need existing state + _options = new BenchmarkWorkspaceOptions(); + } + + [Benchmark(Baseline = true, Description = "Create default BenchmarkWorkspaceOptions")] + [BenchmarkCategory("Construction")] + public BenchmarkWorkspaceOptions CreateDefaultOptions() + { + return new BenchmarkWorkspaceOptions(); + } + + [Benchmark(Description = "Create and configure BenchmarkWorkspaceOptions")] + [BenchmarkCategory("Construction")] + public BenchmarkWorkspaceOptions CreateAndConfigureOptions() + { + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = AppContext.BaseDirectory, + RepositoryTuningFolder = "tuning", + RepositoryReportsFolder = "reports", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net11.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 = "net11.0" + }; + options.PostConfigureOptions(); + options.ValidateOptions(); + GC.KeepAlive(options); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs index 06b45fc..1ff8af9 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs @@ -1,74 +1,74 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -[MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -public class BenchmarkContextBenchmark -{ - private string[] _emptyArgs; - private string[] _smallArgs; - private string[] _mediumArgs; - private string[] _largeArgs; - - [Params(0, 8, 64, 256)] - public int ArgsCount { get; set; } - - [GlobalSetup] - public void Setup() - { - // Deterministic initialization of test data - _emptyArgs = []; - - _smallArgs = new string[8]; - for (int i = 0; i < _smallArgs.Length; i++) - { - _smallArgs[i] = $"--arg{i}"; - } - - _mediumArgs = new string[64]; - for (int i = 0; i < _mediumArgs.Length; i++) - { - _mediumArgs[i] = $"--option{i}"; - } - - _largeArgs = new string[256]; - for (int i = 0; i < _largeArgs.Length; i++) - { - _largeArgs[i] = $"--parameter{i}=value{i}"; - } - } - - [Benchmark(Baseline = true, Description = "Construct BenchmarkContext with empty args")] - public BenchmarkContext ConstructWithEmptyArgs() - { - return new BenchmarkContext(_emptyArgs); - } - - [Benchmark(Description = "Construct BenchmarkContext with null args")] - public BenchmarkContext ConstructWithNullArgs() - { - return new BenchmarkContext(null); - } - - [Benchmark(Description = "Construct BenchmarkContext with varied args count")] - public BenchmarkContext ConstructWithVariedArgs() - { - return ArgsCount switch - { - 0 => new BenchmarkContext(_emptyArgs), - 8 => new BenchmarkContext(_smallArgs), - 64 => new BenchmarkContext(_mediumArgs), - 256 => new BenchmarkContext(_largeArgs), - _ => new BenchmarkContext(_emptyArgs) - }; - } - - [Benchmark(Description = "Access Args property")] - public string[] AccessArgsProperty() - { - var context = new BenchmarkContext(_smallArgs); - return context.Args; - } -} +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkContextBenchmark +{ + private string[] _emptyArgs; + private string[] _smallArgs; + private string[] _mediumArgs; + private string[] _largeArgs; + + [Params(0, 8, 64, 256)] + public int ArgsCount { get; set; } + + [GlobalSetup] + public void Setup() + { + // Deterministic initialization of test data + _emptyArgs = []; + + _smallArgs = new string[8]; + for (int i = 0; i < _smallArgs.Length; i++) + { + _smallArgs[i] = $"--arg{i}"; + } + + _mediumArgs = new string[64]; + for (int i = 0; i < _mediumArgs.Length; i++) + { + _mediumArgs[i] = $"--option{i}"; + } + + _largeArgs = new string[256]; + for (int i = 0; i < _largeArgs.Length; i++) + { + _largeArgs[i] = $"--parameter{i}=value{i}"; + } + } + + [Benchmark(Baseline = true, Description = "Construct BenchmarkContext with empty args")] + public BenchmarkContext ConstructWithEmptyArgs() + { + return new BenchmarkContext(_emptyArgs); + } + + [Benchmark(Description = "Construct BenchmarkContext with null args")] + public BenchmarkContext ConstructWithNullArgs() + { + return new BenchmarkContext(null); + } + + [Benchmark(Description = "Construct BenchmarkContext with varied args count")] + public BenchmarkContext ConstructWithVariedArgs() + { + return ArgsCount switch + { + 0 => new BenchmarkContext(_emptyArgs), + 8 => new BenchmarkContext(_smallArgs), + 64 => new BenchmarkContext(_mediumArgs), + 256 => new BenchmarkContext(_largeArgs), + _ => new BenchmarkContext(_emptyArgs) + }; + } + + [Benchmark(Description = "Access Args property")] + public string[] AccessArgsProperty() + { + var context = new BenchmarkContext(_smallArgs); + return context.Args; + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs index f127580..a5f20a3 100644 --- a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs @@ -1,60 +1,60 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using Cuemon; -using Cuemon.Reflection; -using System.Reflection; - -namespace Codebelt.Extensions.BenchmarkDotNet.Console; - -[MemoryDiagnoser] -[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] -public class BenchmarkProgramBenchmark -{ - private Assembly _testAssembly; - private Assembly _entryAssembly; - - [GlobalSetup] - public void Setup() - { - // Deterministic initialization of test data - _testAssembly = Assembly.GetExecutingAssembly(); - _entryAssembly = Assembly.GetEntryAssembly() ?? _testAssembly; - } - - [Benchmark(Baseline = true, Description = "Access BuildConfiguration property")] - public string AccessBuildConfiguration() - { - return BenchmarkProgram.BuildConfiguration; - } - - [Benchmark(Description = "Access IsDebugBuild property")] - public bool AccessIsDebugBuild() - { - return BenchmarkProgram.IsDebugBuild; - } - - [Benchmark(Description = "Check assembly debug build status")] - public bool CheckAssemblyDebugBuild() - { - return Decorator.Enclose(_testAssembly).IsDebugBuild(); - } - - [Benchmark(Description = "Resolve build configuration from assembly")] - public string ResolveBuildConfiguration() - { - var isDebugBuild = Decorator.Enclose(_testAssembly).IsDebugBuild(); - return isDebugBuild ? "Debug" : "Release"; - } - - [Benchmark(Description = "Check entry assembly debug build status")] - public bool CheckEntryAssemblyDebugBuild() - { - return Decorator.Enclose(_entryAssembly).IsDebugBuild(); - } - - [Benchmark(Description = "Static property access pattern")] - public (string, bool) AccessStaticProperties() - { - return (BenchmarkProgram.BuildConfiguration, BenchmarkProgram.IsDebugBuild); - } -} +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using Cuemon; +using Cuemon.Reflection; +using System.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkProgramBenchmark +{ + private Assembly _testAssembly; + private Assembly _entryAssembly; + + [GlobalSetup] + public void Setup() + { + // Deterministic initialization of test data + _testAssembly = Assembly.GetExecutingAssembly(); + _entryAssembly = Assembly.GetEntryAssembly() ?? _testAssembly; + } + + [Benchmark(Baseline = true, Description = "Access BuildConfiguration property")] + public string AccessBuildConfiguration() + { + return BenchmarkProgram.BuildConfiguration; + } + + [Benchmark(Description = "Access IsDebugBuild property")] + public bool AccessIsDebugBuild() + { + return BenchmarkProgram.IsDebugBuild; + } + + [Benchmark(Description = "Check assembly debug build status")] + public bool CheckAssemblyDebugBuild() + { + return Decorator.Enclose(_testAssembly).IsDebugBuild(); + } + + [Benchmark(Description = "Resolve build configuration from assembly")] + public string ResolveBuildConfiguration() + { + var isDebugBuild = Decorator.Enclose(_testAssembly).IsDebugBuild(); + return isDebugBuild ? "Debug" : "Release"; + } + + [Benchmark(Description = "Check entry assembly debug build status")] + public bool CheckEntryAssemblyDebugBuild() + { + return Decorator.Enclose(_entryAssembly).IsDebugBuild(); + } + + [Benchmark(Description = "Static property access pattern")] + public (string, bool) AccessStaticProperties() + { + return (BenchmarkProgram.BuildConfiguration, BenchmarkProgram.IsDebugBuild); + } +}