diff --git a/.docfx/BuildDocfxImage.ps1 b/.docfx/BuildDocfxImage.ps1 index b0257f4..63fd259 100644 --- a/.docfx/BuildDocfxImage.ps1 +++ b/.docfx/BuildDocfxImage.ps1 @@ -1,4 +1,4 @@ $version = minver -i -t v -v w docfx metadata docfx.json -docker buildx build -t yourbranding/classlibrary1:$version --platform linux/arm64,linux/amd64 --load -f Dockerfile.docfx . +docker buildx build -t benchmarkdotnet-docfx:$version --platform linux/arm64,linux/amd64 --load -f Dockerfile.docfx . get-childItem -recurse -path api -include *.yml, .manifest | remove-item diff --git a/.docfx/PublishDocfxImage.ps1 b/.docfx/PublishDocfxImage.ps1 index b973de7..267de57 100644 --- a/.docfx/PublishDocfxImage.ps1 +++ b/.docfx/PublishDocfxImage.ps1 @@ -1,3 +1,3 @@ $version = minver -i -t v -v w -docker tag classlibrary1-docfx:$version yourbranding/classlibrary1:$version -docker push yourbranding/classlibrary1:$version +docker tag benchmarkdotnet-docfx:$version jcr.codebelt.net/geekle/benchmarkdotnet-docfx:$version +docker push jcr.codebelt.net/geekle/benchmarkdotnet-docfx:$version diff --git a/.docfx/api/namespaces/ClassLibrary1.md b/.docfx/api/namespaces/ClassLibrary1.md deleted file mode 100644 index e4d0124..0000000 --- a/.docfx/api/namespaces/ClassLibrary1.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -uid: ClassLibrary1 -summary: *content ---- -The `ClassLibrary1` namespace contains types that ... - -[!INCLUDE [availability-default](../../includes/availability-default.md)] - -Complements: [xUnit: Capturing Output](https://xunit.net/docs/capturing-output) 🔗 - -### Extension Methods - -|Type|Ext|Methods| -|--:|:-:|---| -|ClassLibrary1|⬇️|`Awesome`| diff --git a/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md new file mode 100644 index 0000000..73b7215 --- /dev/null +++ b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.Console.md @@ -0,0 +1,7 @@ +--- +uid: Codebelt.Extensions.BenchmarkDotNet.Console +summary: *content +--- +The `Codebelt.Extensions.BenchmarkDotNet.Console` namespace contains types that provide a structured and opinionated console-hosted execution model for `BenchmarkDotNet`. + +[!INCLUDE [availability-modern](../../includes/availability-modern.md)] diff --git a/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md new file mode 100644 index 0000000..a1e8fc1 --- /dev/null +++ b/.docfx/api/namespaces/Codebelt.Extensions.BenchmarkDotNet.md @@ -0,0 +1,14 @@ +--- +uid: Codebelt.Extensions.BenchmarkDotNet +summary: *content +--- +The `Codebelt.Extensions.BenchmarkDotNet` namespace contains types that provide a uniform, opinionated, and extensible way of working with `BenchmarkDotNet`. + +[!INCLUDE [availability-modern](../../includes/availability-modern.md)] + +### Extension Methods + +|Type|Ext|Methods| +|--:|:-:|---| +|BenchmarkWorkspaceOptions|⬇️|`ConfigureBenchmarkDotNet`| +|IServiceCollection|⬇️|`AddBenchmarkWorkspace`, `AddBenchmarkWorkspace`| diff --git a/.docfx/docfx.json b/.docfx/docfx.json index 102877c..1988f6b 100644 --- a/.docfx/docfx.json +++ b/.docfx/docfx.json @@ -4,7 +4,8 @@ "src": [ { "files": [ - "ClassLibrary1/**.csproj" + "Codebelt.Extensions.BenchmarkDotNet/**.csproj", + "Codebelt.Extensions.BenchmarkDotNet.Console/**.csproj" ], "src": "../src" } @@ -12,15 +13,14 @@ "dest": "api", "filter": "filterConfig.yml", "properties": { - "TargetFramework": "net8.0" + "TargetFramework": "net10.0" } } ], "build": { "xref": [ "https://docs.cuemon.net/xrefmap.yml", - "https://docs.savvyio.net/xrefmap.yml", - "https://sharedkernel.codebelt.net/xrefmap.yml", + "https://bootstrapper.codebelt.net/xrefmap.yml", "https://github.com/dotnet/docfx/raw/main/.xrefmap.json" ], "content": [ @@ -46,15 +46,15 @@ } ], "globalMetadata": { - "_appTitle": "Shared Kernel (DDD) for .NET", - "_appFooter": "Generated by DocFX. Copyright 2024 ClassLibrary1. All rights reserved.", + "_appTitle": "Extensions for BenchmarkDotNet by Codebelt", + "_appFooter": "Generated by DocFX. Copyright 2025 Geekle. All rights reserved.", "_appLogoPath": "images/50x50.png", "_appFaviconPath": "images/favicon.ico", - "_googleAnalyticsTagId": "G-X000000000", + "_googleAnalyticsTagId": "G-K2NG2TXDWQ", "_enableSearch": false, "_disableContribution": false, "_gitContribute": { - "repo": "https://github.com/classlibrary1/ClassLibrary1", + "repo": "https://github.com/codebeltnet/benchmarkdotnet", "branch": "main" }, "_gitUrlPattern": "github" diff --git a/.docfx/images/32x32.png b/.docfx/images/32x32.png index 9d3ae92..a68f4e4 100644 Binary files a/.docfx/images/32x32.png and b/.docfx/images/32x32.png differ diff --git a/.docfx/images/50x50.png b/.docfx/images/50x50.png index bc05e41..6c4253c 100644 Binary files a/.docfx/images/50x50.png and b/.docfx/images/50x50.png differ diff --git a/.docfx/images/favicon.ico b/.docfx/images/favicon.ico index b3065ba..18debfa 100644 Binary files a/.docfx/images/favicon.ico and b/.docfx/images/favicon.ico differ diff --git a/.docfx/includes/availability-default.md b/.docfx/includes/availability-default.md deleted file mode 100644 index f49ad3f..0000000 --- a/.docfx/includes/availability-default.md +++ /dev/null @@ -1 +0,0 @@ -Availability: .NET 8, .NET 6 and .NET Standard 2.0 \ No newline at end of file diff --git a/.docfx/includes/availability-modern.md b/.docfx/includes/availability-modern.md index c3c49da..544cb79 100644 --- a/.docfx/includes/availability-modern.md +++ b/.docfx/includes/availability-modern.md @@ -1 +1 @@ -Availability: .NET 8 and .NET 6 \ No newline at end of file +Availability: .NET 10 and .NET 9.0 \ No newline at end of file diff --git a/.docfx/index.md b/.docfx/index.md index 9465807..4c9e169 100644 --- a/.docfx/index.md +++ b/.docfx/index.md @@ -1,7 +1,11 @@ --- uid: frontpage-md -title: ClassLibrary1 +title: Extensions for BenchmarkDotNet by Codebelt --- -# ClassLibrary1 +![Extensions for BenchmarkDotNet API by Codebelt](/images/128x128.png) -Landing page for ClassLibrary1. +# Extensions for BenchmarkDotNet by Codebelt + +This project is part of [Extensions for BenchmarkDotNet by Codebelt](https://github.com/codebeltnet/benchmarkdotnet). + +Proceed to the [docs](/api/Codebelt.Extensions.BenchmarkDotNet.html) to learn more about the capabilities of this project. diff --git a/.docfx/packages/index.md b/.docfx/packages/index.md index 563a59d..9802b81 100644 --- a/.docfx/packages/index.md +++ b/.docfx/packages/index.md @@ -1,19 +1,12 @@ # NuGet Packages -This is a list of all NuGet packages from **ClassLibrary1** that is publicly available on [NuGet.org](https://www.nuget.org/packages?q=ClassLibrary1); the packages here are listed alphabetically and are available in preview-, rc- and production-ready versions. +This is a list of all NuGet packages from **Extensions for BenchmarkDotNet by Codebelt** that is publicly available on [NuGet.org](https://www.nuget.org/packages?q=codebelt.extensions.benchmarkdotnet); the packages here are listed alphabetically and are available in preview-, rc- and production-ready versions. ## 📦 Standalone Packages -Provides a focused API for ... +Provides a focused API for BenchmarkDotNet projects. |Package|vNext|Stable|Downloads| |:--|:-:|:-:|:-:| -| [ClassLibrary1](https://www.nuget.org/packages/ClassLibrary1/) | ![vNext](https://img.shields.io/nuget/vpre/ClassLibrary1?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/ClassLibrary1?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/ClassLibrary1?color=blueviolet&logo=nuget) | - -## 🏭 Productivity Packages - -Provides a convenient set of default API additions for ... - -|Package|vNext|Stable|Downloads| -|:--|:-:|:-:|:-:| -| [ClassLibrary1.App](https://www.nuget.org/packages/ClassLibrary1.App/) | ![vNext](https://img.shields.io/nuget/vpre/ClassLibrary1.App?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/ClassLibrary1.App?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/ClassLibrary1.App?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.BenchmarkDotNet?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.BenchmarkDotNet?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.BenchmarkDotNet?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.BenchmarkDotNet.Console?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.BenchmarkDotNet.Console?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.BenchmarkDotNet.Console?color=blueviolet&logo=nuget) | diff --git a/.docfx/toc.yml b/.docfx/toc.yml index 240775d..d360c79 100644 --- a/.docfx/toc.yml +++ b/.docfx/toc.yml @@ -1,4 +1,4 @@ -- name: ClassLibrary1 API - href: api/ClassLibrary1.html +- name: Bootstrapper API + href: api/Codebelt.Extensions.BenchmarkDotNet.html - name: NuGet href: packages diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 12b771e..9ef3035 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,10 +1,10 @@ --- -description: 'Writing Unit Tests in ClassLibrary1' +description: 'Writing Unit Tests in Codebelt.Extensions.BenchmarkDotNet' applyTo: "**/*.{cs,csproj}" --- -# Writing Unit Tests in ClassLibrary1 -This document provides instructions for writing unit tests in the ClassLibrary1 codebase. Please follow these guidelines to ensure consistency and maintainability. +# Writing Unit Tests in Codebelt.Extensions.BenchmarkDotNet +This document provides instructions for writing unit tests in the Codebelt.Extensions.BenchmarkDotNet codebase. Please follow these guidelines to ensure consistency and maintainability. ## 1. Base Class @@ -48,33 +48,33 @@ namespace Your.Namespace ## 5. File and Namespace Organization - Place test files in the appropriate test project and folder structure. -- Use namespaces that mirror the source code structure. The namespace of a test file MUST match the namespace of the System Under Test (SUT). Do NOT append ".Tests", ".Benchmarks" or similar suffixes to the namespace. Only the assembly/project name should indicate that the file is a test/benchmark (for example: ClassLibrary1.Foo.Tests assembly, but namespace ClassLibrary1.Foo). +- Use namespaces that mirror the source code structure. The namespace of a test file MUST match the namespace of the System Under Test (SUT). Do NOT append ".Tests", ".Benchmarks" or similar suffixes to the namespace. Only the assembly/project name should indicate that the file is a test/benchmark (for example: Codebelt.Extensions.BenchmarkDotNet.Foo.Tests assembly, but namespace Codebelt.Extensions.BenchmarkDotNet.Foo). - Example: If the SUT class is declared as: ```csharp - namespace ClassLibrary1.Foo.Bar + namespace Codebelt.Extensions.BenchmarkDotNet.Foo.Bar { public class Zoo { /* ... */ } } ``` then the corresponding unit test class must use the exact same namespace: ```csharp - namespace ClassLibrary1.Foo.Bar + namespace Codebelt.Extensions.BenchmarkDotNet.Foo.Bar { public class ZooTest : Test { /* ... */ } } ``` - Do NOT use: ```csharp - namespace ClassLibrary1.Foo.Bar.Tests { /* ... */ } // ❌ - namespace ClassLibrary1.Foo.Bar.Benchmarks { /* ... */ } // ❌ + namespace Codebelt.Extensions.BenchmarkDotNet.Foo.Bar.Tests { /* ... */ } // ❌ + namespace Codebelt.Extensions.BenchmarkDotNet.Foo.Bar.Benchmarks { /* ... */ } // ❌ ``` -- The unit tests for the ClassLibrary1.Foo assembly live in the ClassLibrary1.Foo.Tests assembly. -- The functional tests for the ClassLibrary1.Foo assembly live in the ClassLibrary1.Foo.FunctionalTests assembly. -- Test class names end with Test and live in the same namespace as the class being tested, e.g., the unit tests for the Boo class that resides in the ClassLibrary1.Foo assembly would be named BooTest and placed in the ClassLibrary1.Foo namespace in the ClassLibrary1.Foo.Tests assembly. +- The unit tests for the Codebelt.Extensions.BenchmarkDotNet.Foo assembly live in the Codebelt.Extensions.BenchmarkDotNet.Foo.Tests assembly. +- The functional tests for the Codebelt.Extensions.BenchmarkDotNet.Foo assembly live in the Codebelt.Extensions.BenchmarkDotNet.Foo.FunctionalTests assembly. +- Test class names end with Test and live in the same namespace as the class being tested, e.g., the unit tests for the Boo class that resides in the Codebelt.Extensions.BenchmarkDotNet.Foo assembly would be named BooTest and placed in the Codebelt.Extensions.BenchmarkDotNet.Foo namespace in the Codebelt.Extensions.BenchmarkDotNet.Foo.Tests assembly. - Modify the associated .csproj file to override the root namespace so the compiled namespace matches the SUT. Example: ```xml - ClassLibrary1.Foo + Codebelt.Extensions.BenchmarkDotNet.Foo ``` - When generating test scaffolding automatically, resolve the SUT's namespace from the source file (or project/assembly metadata) and use that exact namespace in the test file header. @@ -91,7 +91,7 @@ using System.Globalization; using Codebelt.Extensions.Xunit; using Xunit; -namespace ClassLibrary1 +namespace Codebelt.Extensions.BenchmarkDotNet { /// /// Tests for the class. @@ -150,29 +150,29 @@ namespace ClassLibrary1 - Never mock IMarshaller; always use a new instance of JsonMarshaller. --- -description: 'Writing Performance Tests in ClassLibrary1' +description: 'Writing Performance Tests in Codebelt.Extensions.BenchmarkDotNet' applyTo: "tuning/**, **/*Benchmark*.cs" --- -# Writing Performance Tests in ClassLibrary1 -This document provides guidance for writing performance tests (benchmarks) in the ClassLibrary1 codebase using BenchmarkDotNet. Follow these guidelines to keep benchmarks consistent, readable, and comparable. +# Writing Performance Tests in Codebelt.Extensions.BenchmarkDotNet +This document provides guidance for writing performance tests (benchmarks) in the Codebelt.Extensions.BenchmarkDotNet codebase using BenchmarkDotNet. Follow these guidelines to keep benchmarks consistent, readable, and comparable. ## 1. Naming and Placement - Place micro- and component-benchmarks under the `tuning/` folder or in projects named `*.Benchmarks`. - Place benchmark files in the appropriate benchmark project and folder structure. - Use namespaces that mirror the source code structure, e.g. do not suffix with `Benchmarks`. -- Namespace rule: DO NOT append `.Benchmarks` to the namespace. Benchmarks must live in the same namespace as the production assembly. Example: if the production assembly uses `namespace ClassLibrary1.Security.Cryptography`, the benchmark file should also use: +- Namespace rule: DO NOT append `.Benchmarks` to the namespace. Benchmarks must live in the same namespace as the production assembly. Example: if the production assembly uses `namespace Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography`, the benchmark file should also use: ``` - namespace ClassLibrary1.Security.Cryptography + namespace Codebelt.Extensions.BenchmarkDotNet.Security.Cryptography { public class Sha512256Benchmark { /* ... */ } } ``` -The class name may end with `Benchmark`, but the namespace must match the assembly (no `.Benchmarks` suffix). -- The benchmarks for the ClassLibrary1.Bar assembly live in the ClassLibrary1.Bar.Benchmarks assembly. -- Benchmark class names end with Benchmark and live in the same namespace as the class being measured, e.g., the benchmarks for the Zoo class that resides in the ClassLibrary1.Bar assembly would be named ZooBenchmark and placed in the ClassLibrary1.Bar namespace in the ClassLibrary1.Bar.Benchmarks assembly. -- Modify the associated .csproj file to override the root namespace, e.g., ClassLibrary1.Bar. +The class name must end with `Benchmark`, but the namespace must match the assembly (no `.Benchmarks` suffix). +- The benchmarks for the Codebelt.Extensions.BenchmarkDotNet.Bar assembly live in the Codebelt.Extensions.BenchmarkDotNet.Bar.Benchmarks assembly. +- Benchmark class names end with Benchmark and live in the same namespace as the class being measured, e.g., the benchmarks for the Zoo class that resides in the Codebelt.Extensions.BenchmarkDotNet.Bar assembly would be named ZooBenchmark and placed in the Codebelt.Extensions.BenchmarkDotNet.Bar namespace in the Codebelt.Extensions.BenchmarkDotNet.Bar.Benchmarks assembly. +- Modify the associated .csproj file to override the root namespace, e.g., Codebelt.Extensions.BenchmarkDotNet.Bar. ## 2. Attributes and Configuration @@ -203,7 +203,7 @@ The class name may end with `Benchmark`, but the namespace must match the assemb using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; -namespace ClassLibrary1 +namespace Codebelt.Extensions.BenchmarkDotNet { [MemoryDiagnoser] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] @@ -244,11 +244,11 @@ namespace ClassLibrary1 For further examples, refer to the benchmark files under the `tuning/` folder. --- -description: 'Writing XML documentation in ClassLibrary1' +description: 'Writing XML documentation in Codebelt.Extensions.BenchmarkDotNet' applyTo: "**/*.cs" --- -# Writing XML documentation in ClassLibrary1 +# Writing XML documentation in Codebelt.Extensions.BenchmarkDotNet This document provides instructions for writing XML documentation. ## 1. Documentation Style diff --git a/.github/prompts/benchmark.prompt.md b/.github/prompts/benchmark.prompt.md new file mode 100644 index 0000000..b37a123 --- /dev/null +++ b/.github/prompts/benchmark.prompt.md @@ -0,0 +1,164 @@ +--- +mode: agent +description: 'Writing Performance Benchmarks in Codebelt.Extensions.BenchmarkDotNet' +--- + +# Benchmark Fixture Prompt (Codebelt.Extensions.BenchmarkDotNet Tuning Benchmarks) + +This prompt defines how to generate performance tests (“benchmarks”) for the Codebelt.Extensions.BenchmarkDotNet codebase using BenchmarkDotNet. +Benchmarks in Codebelt.Extensions.BenchmarkDotNet are *not* unit tests — they are micro- or component-level performance measurements that belong under the `tuning/` directory and follow strict conventions. + +Copilot must follow these guidelines when generating benchmark fixtures. + +--- + +## 1. Naming and Placement + +- All benchmark projects live under the `tuning/` folder. + Examples: + - `tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/` + - `tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/` + +- **Namespaces must NOT end with `.Benchmarks`.** + They must mirror the production assembly’s namespace. + + Example: + If benchmarking a type inside `Codebelt.Extensions.BenchmarkDotNet.Console`, then: + + ```csharp + namespace Codebelt.Extensions.BenchmarkDotNet.Console + { + public class Sha512256Benchmark { … } + } + ``` + +* **Benchmark class names must end with `Benchmark`.** + Example: `DateSpanBenchmark`, `FowlerNollVoBenchmark`. + +* Benchmark files should be located in the matching benchmark project + (e.g., benchmarks for `Codebelt.Extensions.BenchmarkDotNet.Console` go in `Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj`). + +* In the `.csproj` for each benchmark project, set the root namespace to the production namespace: + + ```xml + Codebelt.Extensions.BenchmarkDotNet.Console + ``` + +--- + +## 2. Attributes and Configuration + +Each benchmark class should use: + +```csharp +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +``` + +Optional but strongly recommended where meaningful: + +* `[Params(...)]` — define small, medium, large input sizes. +* `[GlobalSetup]` — deterministic initialization of benchmark data. +* `[Benchmark(Description = "...")]` — always add descriptions. +* `[Benchmark(Baseline = true)]` — when comparing two implementations. + +Avoid complex global configs; prefer explicit attributes inside the class. + +--- + +## 3. Structure and Best Practices + +A benchmark fixture must: + +* Measure a **single logical operation** per benchmark method. +* Avoid I/O, networking, disk access, logging, or side effects. +* Avoid expensive setup inside `[Benchmark]` methods. +* Use deterministic data (e.g., seeded RNG or predefined constants). +* Use `[GlobalSetup]` to allocate buffers, random payloads, or reusable test data only once. +* Avoid shared mutable state unless reset per iteration. + +Use representative input sizes such as: + +```csharp +[Params(8, 256, 4096)] +public int Count { get; set; } +``` + +BenchmarkDotNet will run each benchmark for each parameter value. + +--- + +## 4. Method Naming Conventions + +Use descriptive names that communicate intent: + +* `Parse_Short` +* `Parse_Long` +* `ComputeHash_Small` +* `ComputeHash_Large` +* `Serialize_Optimized` +* `Serialize_Baseline` + +When comparing approaches, always list them clearly and tag one as the baseline. + +--- + +## 5. Example Benchmark Fixture + +```csharp +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Codebelt.Extensions.BenchmarkDotNet +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class SampleOperationBenchmark + { + [Params(8, 256, 4096)] + public int Count { get; set; } + + private byte[] _payload; + + [GlobalSetup] + public void Setup() + { + _payload = new byte[Count]; + // deterministic initialization + } + + [Benchmark(Baseline = true, Description = "Operation - baseline")] + public int Operation_Baseline() => SampleOperation.Process(_payload); + + [Benchmark(Description = "Operation - optimized")] + public int Operation_Optimized() => SampleOperation.ProcessOptimized(_payload); + } +} +``` + +--- + +## 6. Reporting and CI + +* Benchmark projects live exclusively under `tuning/`. They must not affect production builds. +* Heavy BenchmarkDotNet runs should *not* run in CI unless explicitly configured. +* Reports are produced by the Codebelt.Extensions.BenchmarkDotNet benchmark runner and stored under its configured artifacts directory. + +--- + +## 7. Additional Guidelines + +* Keep benchmark fixtures focused and readable. +* Document non-obvious reasoning in short comments. +* Prefer realistic but deterministic data sets. +* When benchmarks reveal regressions or improvements, reference the associated PR or issue in a comment. +* Shared benchmark helpers belong in `tuning/` projects, not in production code. + +--- + +## Final Notes + +* Benchmarks are performance tests, not unit tests. +* Use `[Benchmark]` only for pure performance measurement. +* Avoid `MethodImplOptions.NoInlining` unless absolutely necessary. +* Use small sets of meaningful benchmark scenarios — avoid combinatorial explosion. diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 6229782..3cc7504 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -1,4 +1,4 @@ -name: ClassLibrary1 CI Pipeline +name: BenchmarkDotNet CI Pipeline on: pull_request: branches: [main] @@ -25,7 +25,7 @@ jobs: uses: codebeltnet/jobs-dotnet-build/.github/workflows/default.yml@v3 with: configuration: ${{ matrix.configuration }} - strong-name-key-filename: classlibrary1.snk + strong-name-key-filename: benchmarkdotnet.snk secrets: inherit pack: @@ -45,21 +45,23 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-24.04, windows-2025, ubuntu-24-04-arm, windows-11-arm] + os: [ubuntu-24.04, windows-2025, ubuntu-24.04-arm, windows-11-arm] configuration: [Debug, Release] uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: configuration: ${{ matrix.configuration }} runs-on: ${{ matrix.os }} build-switches: -p:SkipSignAssembly=true + build: true # we need to build due to xUnitv3 + restore: true # we need to restore due to xUnitv3 sonarcloud: name: call-sonarcloud needs: [build, test] uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 with: - organization: yourorg - projectKey: classlibrary1 + organization: geekle + projectKey: benchmarkdotnet version: ${{ needs.build.outputs.version }} secrets: inherit @@ -68,7 +70,7 @@ jobs: needs: [build, test] uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 with: - repository: yourorg/classlibrary1 + repository: codebeltnet/benchmarkdotnet secrets: inherit codeql: diff --git a/.nuget/ClassLibrary1/PackageReleaseNotes.txt b/.nuget/ClassLibrary1/PackageReleaseNotes.txt deleted file mode 100644 index e2457b4..0000000 --- a/.nuget/ClassLibrary1/PackageReleaseNotes.txt +++ /dev/null @@ -1,9 +0,0 @@ -Version: 0.1.0 -Availability: .NET 10 -  -# ALM -- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) -  -# New Features -- ADDED -  \ No newline at end of file diff --git a/.nuget/ClassLibrary1/README.md b/.nuget/ClassLibrary1/README.md deleted file mode 100644 index 26e0ffa..0000000 --- a/.nuget/ClassLibrary1/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ClassLibrary1 - -An open-source project (MIT license) offering the next great thing withing .NET development. - -Essential code for your ever growing toolbelt of code. diff --git a/.nuget/ClassLibrary1/icon.png b/.nuget/ClassLibrary1/icon.png deleted file mode 100644 index ade21eb..0000000 Binary files a/.nuget/ClassLibrary1/icon.png and /dev/null differ diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt new file mode 100644 index 0000000..59e6169 --- /dev/null +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/PackageReleaseNotes.txt @@ -0,0 +1,8 @@ +Version: 1.0.0 +Availability: .NET 10 and .NET 9 + +# New Features +- ADDED BenchmarkContext class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that represents the command-line context for a benchmark run +- ADDED BenchmarkProgram class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that provides the main entry point for hosting and running benchmarks using BenchmarkDotNet +- ADDED BenchmarkWorker class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that is responsible for executing benchmarks within the console host +  \ No newline at end of file diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md new file mode 100644 index 0000000..5cc9063 --- /dev/null +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/README.md @@ -0,0 +1,46 @@ +# Codebelt.Extensions.BenchmarkDotNet.Console + +A structured, host-based execution model for running BenchmarkDotNet benchmarks in console applications. + +## About + +**Codebelt.Extensions.BenchmarkDotNet.Console** extends the **Codebelt.Extensions.BenchmarkDotNet** package with a dedicated console runner built on the Microsoft Generic Host. + +It provides a predictable startup model, consistent dependency injection, and a managed application lifecycle for benchmark execution, aligning benchmarks with the same hosting principles used in modern .NET applications. + +By embracing `Microsoft.Extensions.Hosting`, this package enables clean separation between benchmark definition, configuration, and execution - making benchmarks easier to compose, test, and evolve over time. + +At its core, the package favors convention over configuration and promotes benchmarks as first-class, host-managed workloads rather than ad-hoc console routines. + +## CSharp Example + +Benchmarks are executed through a console-hosted Generic Host, allowing full participation in dependency injection, configuration, logging and more. + +```csharp +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using Codebelt.Extensions.BenchmarkDotNet.Console; + +public class Program +{ + public static void Main(string[] args) + { + BenchmarkProgram.Run(args, o => + { + o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; + o.ConfigureBenchmarkDotNet(c => + { + var slimJob = BenchmarkWorkspaceOptions.Slim; + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) + .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + }); + }); + } +} +``` + +## Related Packages + +* [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) 📦 +* [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) 📦 diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/icon.png b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/icon.png new file mode 100644 index 0000000..0b304b4 Binary files /dev/null and b/.nuget/Codebelt.Extensions.BenchmarkDotNet.Console/icon.png differ diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt new file mode 100644 index 0000000..42108fa --- /dev/null +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/PackageReleaseNotes.txt @@ -0,0 +1,10 @@ +Version: 1.0.0 +Availability: .NET 10 and .NET 9 + +# New Features +- ADDED BenchmarkWorkspace class in the Codebelt.Extensions.BenchmarkDotNet namespace that provides a default implementation of IBenchmarkWorkspace for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet +- ADDED BenchmarkWorkspaceOptions class in the Codebelt.Extensions.BenchmarkDotNet namespace that specifies configuration options that is related to the BenchmarkWorkspace class +- ADDED BenchmarkWorkspaceOptionsExtensions class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the BenchmarkWorkspaceOptions class: ConfigureBenchmarkDotNet +- ADDED IBenchmarkWorkspace interface in the Codebelt.Extensions.BenchmarkDotNet namespace that defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet +- ADDED ServiceCollectionExtensions class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the IServiceCollection interface: AddBenchmarkWorkspace and AddBenchmarkWorkspace{TWorkspace} +  \ No newline at end of file diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md b/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md new file mode 100644 index 0000000..88556e8 --- /dev/null +++ b/.nuget/Codebelt.Extensions.BenchmarkDotNet/README.md @@ -0,0 +1,78 @@ +# Codebelt.Extensions.BenchmarkDotNet + +A unified, opinionated foundation for building robust BenchmarkDotNet workflows in .NET. + +## About + +**Codebelt.Extensions.BenchmarkDotNet** is part of a modern, MIT-licensed ecosystem designed to bring clarity, structure, and consistency to BenchmarkDotNet projects. + +If you value predictable conventions, clean separation of responsibilities, and benchmarks that scale gracefully across `.NET 9` and `.NET 10`, this library is your agile companion. + +It removes unnecessary ceremony while embracing best practices from other consumers of BenchmarkDotNet, so you can focus on performance insights, not plumbing. + +At its heart, the package is **free, flexible, and crafted to extend and empower your agile codebelt**. + +## Folder Structure + +The folder structure promoted by **Codebelt.Extensions.BenchmarkDotNet** follows the same architectural principles commonly used for test projects—while remaining purpose-built for benchmarking. + +At the solution level, benchmarks are treated as a first-class concern, clearly separated from tooling and output artifacts. + +- **tuning** contains all benchmark projects (e.g. `*.Benchmarks`), in the same way that a `test` folder typically contains `*.Tests` projects, +- **tooling** hosts the executable console application responsible for discovering and running benchmarks, +- **reports** captures benchmark results and generated artifacts, separated from source code and tooling concerns. + +This separation enforces a clean boundary between benchmark definition, execution, and output, making benchmark suites easier to scale, automate, and reason about. + +### Example Layout + +```text +Repository Root +│ +├─ reports +│ └─ tuning +│ └─ github +│ └─ MyLibrary.ExampleBenchmarks-report-github.md +│ +├─ src +│ └─ MyLibrary +│ +├─ test +│ └─ MyLibrary.Tests +│ └─ ExampleTest.cs +│ +├─ tooling +│ └─ benchmark-runner +│ └─ Program.cs +│ +└─ tuning + └─ MyLibrary.Benchmarks + └─ ExampleBenchmark.cs +``` + + +## CSharp Example + +Benchmarks are executed using a Generic Host–based bootstrap model, allowing BenchmarkDotNet to participate in a fully managed application lifecycle with dependency injection, configuration, and logging. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; + +var hostBuilder = Host.CreateDefaultBuilder(args); +hostBuilder.ConfigureServices(services => +{ + services.AddSingleton(new BenchmarkContext(args)); + services.AddBenchmarkWorkspace(setup); +}); +var host = hostBuilder.Build(); +host.Run(); +``` + +The folder structure is based o + +## Related Packages + +* [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) 📦 +* [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) 📦 diff --git a/.nuget/Codebelt.Extensions.BenchmarkDotNet/icon.png b/.nuget/Codebelt.Extensions.BenchmarkDotNet/icon.png new file mode 100644 index 0000000..0b304b4 Binary files /dev/null and b/.nuget/Codebelt.Extensions.BenchmarkDotNet/icon.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index f0eac06..f41a179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. -## [Unreleased] - TBD +## [1.0.0] - 2025-12-12 + +This is the initial stable release of the `Codebelt.Extensions.BenchmarkDotNet` and `Codebelt.Extensions.BenchmarkDotNet.Console` packages. ### Added + +- ADDED `BenchmarkWorkspace` class in the Codebelt.Extensions.BenchmarkDotNet namespace that provides a default implementation of `IBenchmarkWorkspace` for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet, +- ADDED `BenchmarkWorkspaceOptions` class in the Codebelt.Extensions.BenchmarkDotNet namespace that specifies configuration options that is related to the `BenchmarkWorkspace` class, +- ADDED `BenchmarkWorkspaceOptionsExtensions` class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the `BenchmarkWorkspaceOptions` class: `ConfigureBenchmarkDotNet`, +- ADDED `IBenchmarkWorkspace` interface in the Codebelt.Extensions.BenchmarkDotNet namespace that defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet, +- ADDED `ServiceCollectionExtensions` class in the Codebelt.Extensions.BenchmarkDotNet namespace that consist of extension methods for the IServiceCollection interface: `AddBenchmarkWorkspace` and `AddBenchmarkWorkspace{TWorkspace}`, +- ADDED `BenchmarkContext` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that represents the command-line context for a benchmark run, +- ADDED `BenchmarkProgram` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that provides the main entry point for hosting and running benchmarks using BenchmarkDotNet, +- ADDED `BenchmarkWorker` class in the Codebelt.Extensions.BenchmarkDotNet.Console namespace that is responsible for executing benchmarks within the console host. diff --git a/ClassLibrary1.sln b/ClassLibrary1.sln deleted file mode 100644 index 4f2a9f7..0000000 --- a/ClassLibrary1.sln +++ /dev/null @@ -1,46 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34728.123 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClassLibrary1", "src\ClassLibrary1\ClassLibrary1.csproj", "{A9DFF36B-1AD4-40EC-9394-C720C3DC785A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0070E83B-2DDD-4537-A83F-1CF8644F2880}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A3C56B2E-55EE-44EC-876E-B03B8DDA3317}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject1.Tests", "test\TestProject1\TestProject1.Tests.csproj", "{A7389E99-2E98-4925-8055-3267BBC6C084}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1.FunctionalTests", "test\TestProject1.FunctionalTests\TestProject1.FunctionalTests.csproj", "{507C6397-4FE2-40E8-A8AA-68ED202B48C8}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A}.Release|Any CPU.Build.0 = Release|Any CPU - {A7389E99-2E98-4925-8055-3267BBC6C084}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A7389E99-2E98-4925-8055-3267BBC6C084}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A7389E99-2E98-4925-8055-3267BBC6C084}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A7389E99-2E98-4925-8055-3267BBC6C084}.Release|Any CPU.Build.0 = Release|Any CPU - {507C6397-4FE2-40E8-A8AA-68ED202B48C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {507C6397-4FE2-40E8-A8AA-68ED202B48C8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {507C6397-4FE2-40E8-A8AA-68ED202B48C8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {507C6397-4FE2-40E8-A8AA-68ED202B48C8}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {A9DFF36B-1AD4-40EC-9394-C720C3DC785A} = {0070E83B-2DDD-4537-A83F-1CF8644F2880} - {A7389E99-2E98-4925-8055-3267BBC6C084} = {A3C56B2E-55EE-44EC-876E-B03B8DDA3317} - {507C6397-4FE2-40E8-A8AA-68ED202B48C8} = {A3C56B2E-55EE-44EC-876E-B03B8DDA3317} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {0CBE2805-F0FF-4D0F-902C-8B9277A5D3F2} - EndGlobalSection -EndGlobal diff --git a/Codebelt.Extensions.BenchmarkDotNet.slnx b/Codebelt.Extensions.BenchmarkDotNet.slnx new file mode 100644 index 0000000..bc97d92 --- /dev/null +++ b/Codebelt.Extensions.BenchmarkDotNet.slnx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Directory.Build.props b/Directory.Build.props index 933c608..1516ab1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,4 @@ - $(MSBuildProjectName.EndsWith('Tests')) $(MSBuildProjectName.EndsWith('Benchmarks')) @@ -9,6 +8,7 @@ $([MSBuild]::IsOSPlatform('Windows')) true false + ..\..\.nuget\$(MSBuildProjectName)\PackageReleaseNotes.txt latest @@ -18,16 +18,16 @@ - net10.0 - Copyright © ClassLibrary1 2025. All rights reserved. - ClassLibrary1 - ClassLibrary1 - ClassLibrary1 + net10.0;net9.0 + Copyright © Geekle 2025. All rights reserved. + gimlichael + Geekle + Extensions for BenchmarkDotNet API by Codebelt icon.png README.md - https://www.classlibrary1.net/ + https://benchmarkdotnet.codebelt.net/ MIT - https://github.com/classlibrary1/ClassLibrary1 + https://github.com/codebeltnet/benchmarkdotnet git en-US true @@ -36,7 +36,7 @@ snupkg true true - $(MSBuildThisFileDirectory)ClassLibrary1.snk + $(MSBuildThisFileDirectory)benchmarkdotnet.snk true latest Recommended @@ -46,7 +46,7 @@ - + @@ -55,15 +55,8 @@ - - net10.0 - - - - net10.0 - - + net10.0;net9.0 false Exe false @@ -107,12 +100,20 @@ - net10.0 + net10.0;net9.0 + false + false + false + false + true + none + NU1701,NU1902,NU1903 + false + false - diff --git a/Directory.Build.targets b/Directory.Build.targets index 05374a0..f2f12ec 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,8 +1,4 @@  - - ..\..\.nuget\$(MSBuildProjectName)\PackageReleaseNotes.txt - - @@ -18,4 +14,8 @@ $(MinVerMajor).$(MinVerMinor).$(MinVerPatch).$(GITHUB_RUN_NUMBER) + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 536d017..e454e68 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,10 +3,12 @@ true - - - - + + + + + + diff --git a/README.md b/README.md index b5cf6c1..52585e3 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,99 @@ -![ClassLibrary1](.nuget/ClassLibrary1/icon.png) +![Extensions for BenchmarkDotNet API by Codebelt](.nuget/Codebelt.Extensions.BenchmarkDotNet/icon.png) -# Classlibrary1 API by Codebelt +# Extensions for BenchmarkDotNet API by Codebelt -[![ClassLibrary1 CI/CD Pipeline](https://github.com/codebeltnet/ClassLibrary1/actions/workflows/pipelines.yml/badge.svg)](https://github.com/codebeltnet/ClassLibrary1/actions/workflows/pipelines.yml) [![codecov](https://codecov.io/gh/codebeltnet/ClassLibrary1/graph/badge.svg?token=WAmfmpQyCz)](https://codecov.io/gh/codebeltnet/ClassLibrary1) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ClassLibrary1&metric=alert_status)](https://sonarcloud.io/dashboard?id=ClassLibrary1) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ClassLibrary1&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=ClassLibrary1) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=ClassLibrary1&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=ClassLibrary1) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ClassLibrary1&metric=security_rating)](https://sonarcloud.io/dashboard?id=ClassLibrary1) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/codebeltnet/ClassLibrary1/badge)](https://scorecard.dev/viewer/?uri=github.com/codebeltnet/ClassLibrary1) +[![BenchmarkDotNet CI/CD Pipeline](https://github.com/codebeltnet/benchmarkdotnet/actions/workflows/ci-pipeline.yml/badge.svg)](https://github.com/codebeltnet/benchmarkdotnet/actions/workflows/ci-pipeline.yml)[![codecov](https://codecov.io/gh/codebeltnet/benchmarkdotnet/graph/badge.svg?token=qX3lHGvFS4)](https://codecov.io/gh/codebeltnet/benchmarkdotnet) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=benchmarkdotnet&metric=alert_status)](https://sonarcloud.io/dashboard?id=benchmarkdotnet) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=benchmarkdotnet&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=benchmarkdotnet) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=benchmarkdotnet&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=benchmarkdotnet) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=benchmarkdotnet&metric=security_rating)](https://sonarcloud.io/dashboard?id=benchmarkdotnet) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/codebeltnet/benchmarkdotnet/badge)](https://scorecard.dev/viewer/?uri=github.com/codebeltnet/benchmarkdotnet) -Provides a focused API for .NET class library projects following [Microsoft Engineering Guidelines](https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines) as well as Conventions, Idioms and Patterns by [Codebelt](https://github.com/codebeltnet#conventions-idioms-and-patterns). +An open-source project (MIT license) that targets and complements the [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) performance library. It provides a uniform and convenient way of doing benchmarking for all project types in .NET. -Full documentation (generated by [DocFx](https://github.com/dotnet/docfx)) located here: https://xxx.yyy.zzz/ +Your versatile BenchmarkDotNet companion for modern development with `.NET 9` and `.NET 10`. -## 📦 Standalone Packages +It is, by heart, free, flexible and built to extend and boost your agile codebelt. -Provides a focused API for ... +## Concept -|Package|vNext|Stable|Downloads| -|:--|:-:|:-:|:-:| -| [ClassLibrary1](https://www.nuget.org/packages/ClassLibrary1/) | ![vNext](https://img.shields.io/nuget/vpre/ClassLibrary1?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/ClassLibrary1?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/ClassLibrary1?color=blueviolet&logo=nuget) | +The Extensions for BenchmarkDotNet API by Codebelt is designed to bring clarity, structure, and consistency to BenchmarkDotNet projects. + +### Folder Structure + +The folder structure promoted by **Codebelt.Extensions.BenchmarkDotNet** follows the same architectural principles commonly used for test projects—while remaining purpose-built for benchmarking. + +At the solution level, benchmarks are treated as a first-class concern, clearly separated from tooling and output artifacts. + +- **tuning** contains all benchmark projects (e.g. `*.Benchmarks`), in the same way that a `test` folder typically contains `*.Tests` projects, +- **tooling** hosts the executable console application responsible for discovering and running benchmarks, +- **reports** captures benchmark results and generated artifacts, separated from source code and tooling concerns. + +This separation enforces a clean boundary between benchmark definition, execution, and output, making benchmark suites easier to scale, automate, and reason about. + +### Layout Example -## 🏭 Productivity Packages +```text +Repository Root +│ +├─ reports +│ └─ tuning +│ └─ github +│ └─ MyLibrary.ExampleBenchmarks-report-github.md +│ +├─ src +│ └─ MyLibrary +│ +├─ test +│ └─ MyLibrary.Tests +│ └─ ExampleTest.cs +│ +├─ tooling +│ └─ benchmark-runner +│ └─ Program.cs +│ +└─ tuning + └─ MyLibrary.Benchmarks + └─ ExampleBenchmark.cs +``` + +### Codebelt.Extensions.BenchmarkDotNet.Console Example + +Benchmarks are executed through a console-hosted Generic Host, allowing full participation in dependency injection, configuration, logging and more. + +```csharp +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using Codebelt.Extensions.BenchmarkDotNet.Console; + +public class Program +{ + public static void Main(string[] args) + { + BenchmarkProgram.Run(args, o => + { + o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; + o.ConfigureBenchmarkDotNet(c => + { + var slimJob = BenchmarkWorkspaceOptions.Slim; + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) + .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + }); + }); + } +} +``` + +## 📚 Documentation + +Full documentation (generated by [DocFx](https://github.com/dotnet/docfx)) located here: https://benchmarkdotnet.codebelt.net/ + +## 📦 Standalone Packages -Provides a convenient set of default API additions for ... +Provides a focused API for BenchmarkDotNet projects. |Package|vNext|Stable|Downloads| |:--|:-:|:-:|:-:| -| [ClassLibrary1.App](https://www.nuget.org/packages/ClassLibrary1.App/) | ![vNext](https://img.shields.io/nuget/vpre/ClassLibrary1.App?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/ClassLibrary1.App?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/ClassLibrary1.App?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.BenchmarkDotNet](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.BenchmarkDotNet?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.BenchmarkDotNet?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.BenchmarkDotNet?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.BenchmarkDotNet.Console](https://www.nuget.org/packages/Codebelt.Extensions.BenchmarkDotNet.Console/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.BenchmarkDotNet.Console?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.BenchmarkDotNet.Console?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.BenchmarkDotNet.Console?color=blueviolet&logo=nuget) | -### Contributing to `Extensions for ClassLibrary1 API by Codebelt` +### Contributing to `Extensions for BenchmarkDotNet API by Codebelt` [Contributions](.github/CONTRIBUTING.md) are welcome and appreciated. Feel free to submit issues, feature requests, or pull requests to help improve this library. diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md new file mode 100644 index 0000000..6747bf6 --- /dev/null +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceBenchmark-report-github.md @@ -0,0 +1,22 @@ +``` + +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|-------------------------------------------------------------- |---------- |-----------:|-----------:|-----------:|-----------:|-----------:|-----------:|-------:|--------:|-------:|----------:|------------:| +| 'Construct BenchmarkDotNetWorkspace' | .NET 10.0 | 2.162 μs | 0.0289 μs | 0.0241 μs | 2.162 μs | 2.131 μs | 2.213 μs | 1.00 | 0.02 | 0.2678 | 4248 B | 1.00 | +| 'Load assemblies from tuning folder (no matching assemblies)' | .NET 10.0 | 434.051 μs | 32.0943 μs | 36.9599 μs | 422.094 μs | 395.167 μs | 523.232 μs | 200.80 | 16.84 | - | 15913 B | 3.75 | +| 'PostProcessArtifacts (move results -> tuning folder)' | .NET 10.0 | 10.441 μs | 0.2018 μs | 0.2073 μs | 10.398 μs | 10.196 μs | 10.922 μs | 4.83 | 0.11 | - | 392 B | 0.09 | +| | | | | | | | | | | | | | +| 'Construct BenchmarkDotNetWorkspace' | .NET 9.0 | 3.030 μs | 0.0734 μs | 0.0815 μs | 3.055 μs | 2.885 μs | 3.190 μs | 1.00 | 0.04 | 0.2628 | 4272 B | 1.00 | +| 'Load assemblies from tuning folder (no matching assemblies)' | .NET 9.0 | 422.517 μs | 13.7315 μs | 15.8133 μs | 416.600 μs | 405.125 μs | 460.289 μs | 139.56 | 6.30 | - | 15817 B | 3.70 | +| 'PostProcessArtifacts (move results -> tuning folder)' | .NET 9.0 | 10.349 μs | 0.2326 μs | 0.2585 μs | 10.293 μs | 9.973 μs | 10.872 μs | 3.42 | 0.12 | - | 392 B | 0.09 | diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md new file mode 100644 index 0000000..bea3e61 --- /dev/null +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspaceOptionsBenchmark-report-github.md @@ -0,0 +1,44 @@ +``` + +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------------------------------------- |---------- |--------------:|-----------:|-----------:|--------------:|--------------:|--------------:|------:|--------:|-------:|----------:|------------:| +| 'PostConfigureOptions - default config' | .NET 10.0 | 2,078.7319 ns | 58.4506 ns | 62.5414 ns | 2,071.1990 ns | 1,973.5708 ns | 2,178.6508 ns | ? | ? | 0.2657 | 4248 B | ? | +| 'PostConfigureOptions - custom config' | .NET 10.0 | 2,063.4760 ns | 62.8084 ns | 69.8114 ns | 2,058.5810 ns | 1,975.1579 ns | 2,212.9844 ns | ? | ? | 0.3027 | 4840 B | ? | +| | | | | | | | | | | | | | +| 'PostConfigureOptions - default config' | .NET 9.0 | 2,933.8417 ns | 57.4521 ns | 53.7407 ns | 2,921.3425 ns | 2,860.3389 ns | 3,030.9748 ns | ? | ? | 0.2644 | 4248 B | ? | +| 'PostConfigureOptions - custom config' | .NET 9.0 | 3,027.1892 ns | 69.3406 ns | 77.0719 ns | 3,014.0595 ns | 2,911.8273 ns | 3,165.0277 ns | ? | ? | 0.3041 | 4840 B | ? | +| | | | | | | | | | | | | | +| 'Create default BenchmarkWorkspaceOptions' | .NET 10.0 | 2,112.7347 ns | 40.1347 ns | 42.9437 ns | 2,107.3630 ns | 2,023.5091 ns | 2,166.5292 ns | 1.00 | 0.03 | 0.2575 | 4120 B | 1.00 | +| 'Create and configure BenchmarkWorkspaceOptions' | .NET 10.0 | 2,060.7947 ns | 40.9970 ns | 43.8663 ns | 2,051.6384 ns | 1,997.5080 ns | 2,135.4734 ns | 0.98 | 0.03 | 0.2585 | 4120 B | 1.00 | +| | | | | | | | | | | | | | +| 'Create default BenchmarkWorkspaceOptions' | .NET 9.0 | 3,008.0985 ns | 41.5610 ns | 36.8428 ns | 3,006.9752 ns | 2,957.3965 ns | 3,084.6125 ns | 1.00 | 0.02 | 0.2564 | 4120 B | 1.00 | +| 'Create and configure BenchmarkWorkspaceOptions' | .NET 9.0 | 3,018.5043 ns | 64.9885 ns | 72.2346 ns | 3,020.6452 ns | 2,898.3530 ns | 3,166.4212 ns | 1.00 | 0.03 | 0.2543 | 4120 B | 1.00 | +| | | | | | | | | | | | | | +| 'Full lifecycle - create, configure, validate' | .NET 10.0 | 2,133.8326 ns | 41.8069 ns | 39.1062 ns | 2,127.8066 ns | 2,091.4368 ns | 2,212.8536 ns | ? | ? | 0.2765 | 4464 B | ? | +| | | | | | | | | | | | | | +| 'Full lifecycle - create, configure, validate' | .NET 9.0 | 3,059.8789 ns | 67.8953 ns | 78.1884 ns | 3,053.1393 ns | 2,965.1217 ns | 3,244.5957 ns | ? | ? | 0.2740 | 4464 B | ? | +| | | | | | | | | | | | | | +| 'Property access - RepositoryPath' | .NET 10.0 | 0.6729 ns | 0.0517 ns | 0.0484 ns | 0.6575 ns | 0.6005 ns | 0.7820 ns | ? | ? | - | - | ? | +| 'Property access - Configuration' | .NET 10.0 | 0.6834 ns | 0.0442 ns | 0.0414 ns | 0.6914 ns | 0.6219 ns | 0.7441 ns | ? | ? | - | - | ? | +| | | | | | | | | | | | | | +| 'Property access - RepositoryPath' | .NET 9.0 | 0.7156 ns | 0.0646 ns | 0.0604 ns | 0.6920 ns | 0.6508 ns | 0.8309 ns | ? | ? | - | - | ? | +| 'Property access - Configuration' | .NET 9.0 | 0.7295 ns | 0.0387 ns | 0.0362 ns | 0.7216 ns | 0.6821 ns | 0.8011 ns | ? | ? | - | - | ? | +| | | | | | | | | | | | | | +| 'Property modification - set all properties' | .NET 10.0 | 2,145.0926 ns | 39.4528 ns | 34.9739 ns | 2,140.4873 ns | 2,100.3063 ns | 2,195.2403 ns | ? | ? | 0.2594 | 4120 B | ? | +| | | | | | | | | | | | | | +| 'Property modification - set all properties' | .NET 9.0 | 2,958.1106 ns | 65.5500 ns | 72.8586 ns | 2,931.3603 ns | 2,871.0808 ns | 3,147.9898 ns | ? | ? | 0.2529 | 4120 B | ? | +| | | | | | | | | | | | | | +| 'ValidateOptions - valid state' | .NET 10.0 | 7.0237 ns | 0.0981 ns | 0.0870 ns | 7.0394 ns | 6.8917 ns | 7.1845 ns | ? | ? | - | - | ? | +| | | | | | | | | | | | | | +| 'ValidateOptions - valid state' | .NET 9.0 | 3.9379 ns | 0.1053 ns | 0.1081 ns | 3.9329 ns | 3.7514 ns | 4.1705 ns | ? | ? | - | - | ? | diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkContextBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkContextBenchmark-report-github.md new file mode 100644 index 0000000..a4f5a04 --- /dev/null +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkContextBenchmark-report-github.md @@ -0,0 +1,54 @@ +``` + +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Runtime | ArgsCount | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|---------------------------------------------------- |---------- |---------- |----------:|----------:|----------:|----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| +| **'Construct BenchmarkContext with empty args'** | **.NET 10.0** | **0** | **4.3308 ns** | **0.1697 ns** | **0.1887 ns** | **4.3068 ns** | **4.0916 ns** | **4.7537 ns** | **1.00** | **0.06** | **0.0015** | **24 B** | **1.00** | +| 'Construct BenchmarkContext with null args' | .NET 10.0 | 0 | 2.9383 ns | 0.2773 ns | 0.3082 ns | 2.8849 ns | 2.6235 ns | 3.6384 ns | 0.68 | 0.07 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 10.0 | 0 | 3.7137 ns | 0.1044 ns | 0.0976 ns | 3.6983 ns | 3.5100 ns | 3.8736 ns | 0.86 | 0.04 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 10.0 | 0 | 0.6430 ns | 0.0409 ns | 0.0362 ns | 0.6344 ns | 0.5997 ns | 0.7230 ns | 0.15 | 0.01 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| 'Construct BenchmarkContext with empty args' | .NET 9.0 | 0 | 3.8301 ns | 0.2265 ns | 0.2517 ns | 3.8544 ns | 3.4316 ns | 4.3832 ns | 1.00 | 0.09 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with null args' | .NET 9.0 | 0 | 3.5669 ns | 0.1647 ns | 0.1692 ns | 3.5704 ns | 3.3070 ns | 3.9297 ns | 0.94 | 0.07 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 9.0 | 0 | 3.7700 ns | 0.2227 ns | 0.2475 ns | 3.7711 ns | 3.4919 ns | 4.4114 ns | 0.99 | 0.09 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 9.0 | 0 | 0.7330 ns | 0.0683 ns | 0.0701 ns | 0.7152 ns | 0.6434 ns | 0.8802 ns | 0.19 | 0.02 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| **'Construct BenchmarkContext with empty args'** | **.NET 10.0** | **8** | **3.1951 ns** | **0.3348 ns** | **0.3856 ns** | **3.1650 ns** | **2.6105 ns** | **3.7070 ns** | **1.01** | **0.17** | **0.0015** | **24 B** | **1.00** | +| 'Construct BenchmarkContext with null args' | .NET 10.0 | 8 | 3.2411 ns | 0.3719 ns | 0.4283 ns | 3.2930 ns | 2.5685 ns | 4.2290 ns | 1.03 | 0.18 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 10.0 | 8 | 3.1851 ns | 0.2819 ns | 0.3133 ns | 3.0243 ns | 2.7742 ns | 3.8016 ns | 1.01 | 0.16 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 10.0 | 8 | 0.6626 ns | 0.0460 ns | 0.0430 ns | 0.6569 ns | 0.6087 ns | 0.7280 ns | 0.21 | 0.03 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| 'Construct BenchmarkContext with empty args' | .NET 9.0 | 8 | 3.7102 ns | 0.1381 ns | 0.1535 ns | 3.7001 ns | 3.5006 ns | 4.0536 ns | 1.00 | 0.06 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with null args' | .NET 9.0 | 8 | 3.7332 ns | 0.1588 ns | 0.1630 ns | 3.7490 ns | 3.5074 ns | 4.1163 ns | 1.01 | 0.06 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 9.0 | 8 | 3.8434 ns | 0.2557 ns | 0.2945 ns | 3.7878 ns | 3.4957 ns | 4.5042 ns | 1.04 | 0.09 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 9.0 | 8 | 0.6815 ns | 0.0474 ns | 0.0444 ns | 0.6644 ns | 0.6268 ns | 0.7629 ns | 0.18 | 0.01 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| **'Construct BenchmarkContext with empty args'** | **.NET 10.0** | **64** | **2.8310 ns** | **0.1972 ns** | **0.2192 ns** | **2.7632 ns** | **2.6250 ns** | **3.4390 ns** | **1.01** | **0.10** | **0.0015** | **24 B** | **1.00** | +| 'Construct BenchmarkContext with null args' | .NET 10.0 | 64 | 2.8201 ns | 0.1224 ns | 0.1360 ns | 2.8519 ns | 2.6071 ns | 3.0278 ns | 1.00 | 0.08 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 10.0 | 64 | 2.9536 ns | 0.1123 ns | 0.1153 ns | 2.9485 ns | 2.7572 ns | 3.1038 ns | 1.05 | 0.08 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 10.0 | 64 | 0.6605 ns | 0.0620 ns | 0.0580 ns | 0.6445 ns | 0.5776 ns | 0.7656 ns | 0.23 | 0.03 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| 'Construct BenchmarkContext with empty args' | .NET 9.0 | 64 | 3.7801 ns | 0.2699 ns | 0.3108 ns | 3.6587 ns | 3.4339 ns | 4.3658 ns | 1.01 | 0.11 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with null args' | .NET 9.0 | 64 | 4.0339 ns | 0.3728 ns | 0.4293 ns | 3.9704 ns | 3.4332 ns | 4.9281 ns | 1.07 | 0.14 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 9.0 | 64 | 4.1926 ns | 0.3751 ns | 0.4320 ns | 4.3145 ns | 3.6149 ns | 4.8710 ns | 1.12 | 0.14 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 9.0 | 64 | 0.7207 ns | 0.0680 ns | 0.0636 ns | 0.6908 ns | 0.6453 ns | 0.8471 ns | 0.19 | 0.02 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| **'Construct BenchmarkContext with empty args'** | **.NET 10.0** | **256** | **2.7304 ns** | **0.1031 ns** | **0.1059 ns** | **2.6816 ns** | **2.5805 ns** | **2.9866 ns** | **1.00** | **0.05** | **0.0015** | **24 B** | **1.00** | +| 'Construct BenchmarkContext with null args' | .NET 10.0 | 256 | 2.7992 ns | 0.1377 ns | 0.1530 ns | 2.8273 ns | 2.5493 ns | 3.1046 ns | 1.03 | 0.07 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 10.0 | 256 | 3.0138 ns | 0.1205 ns | 0.1339 ns | 2.9849 ns | 2.8299 ns | 3.2886 ns | 1.11 | 0.06 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 10.0 | 256 | 0.6323 ns | 0.0360 ns | 0.0281 ns | 0.6335 ns | 0.5944 ns | 0.6804 ns | 0.23 | 0.01 | - | - | 0.00 | +| | | | | | | | | | | | | | | +| 'Construct BenchmarkContext with empty args' | .NET 9.0 | 256 | 4.7236 ns | 0.1670 ns | 0.1856 ns | 4.6982 ns | 4.5087 ns | 5.1179 ns | 1.00 | 0.05 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with null args' | .NET 9.0 | 256 | 3.5940 ns | 0.1319 ns | 0.1466 ns | 3.5709 ns | 3.3897 ns | 3.9573 ns | 0.76 | 0.04 | 0.0015 | 24 B | 1.00 | +| 'Construct BenchmarkContext with varied args count' | .NET 9.0 | 256 | 3.7935 ns | 0.1367 ns | 0.1520 ns | 3.8089 ns | 3.5272 ns | 4.0886 ns | 0.80 | 0.04 | 0.0015 | 24 B | 1.00 | +| 'Access Args property' | .NET 9.0 | 256 | 0.6915 ns | 0.0522 ns | 0.0436 ns | 0.6913 ns | 0.6360 ns | 0.8050 ns | 0.15 | 0.01 | - | - | 0.00 | diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkProgramBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkProgramBenchmark-report-github.md new file mode 100644 index 0000000..459881b --- /dev/null +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkProgramBenchmark-report-github.md @@ -0,0 +1,28 @@ +``` + +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|-------------------------------------------- |---------- |--------------:|------------:|------------:|--------------:|--------------:|--------------:|----------:|----------:|-------:|----------:|------------:| +| 'Access BuildConfiguration property' | .NET 10.0 | 0.0610 ns | 0.0436 ns | 0.0386 ns | 0.0570 ns | 0.0108 ns | 0.1273 ns | 1.72 | 2.01 | - | - | NA | +| 'Access IsDebugBuild property' | .NET 10.0 | 0.0048 ns | 0.0127 ns | 0.0106 ns | 0.0000 ns | 0.0000 ns | 0.0356 ns | 0.13 | 0.40 | - | - | NA | +| 'Check assembly debug build status' | .NET 10.0 | 2,062.5568 ns | 227.9485 ns | 243.9024 ns | 1,989.5074 ns | 1,820.6402 ns | 2,634.0309 ns | 58,090.94 | 50,285.20 | 0.5438 | 8793 B | NA | +| 'Resolve build configuration from assembly' | .NET 10.0 | 2,098.8584 ns | 223.0681 ns | 238.6804 ns | 1,976.7257 ns | 1,880.0177 ns | 2,626.4587 ns | 59,113.36 | 51,111.52 | 0.5473 | 8793 B | NA | +| 'Check entry assembly debug build status' | .NET 10.0 | 2,150.8535 ns | 68.8797 ns | 67.6491 ns | 2,123.1321 ns | 2,072.9937 ns | 2,304.4094 ns | 60,577.77 | 51,701.90 | 0.5289 | 8529 B | NA | +| 'Static property access pattern' | .NET 10.0 | 0.0582 ns | 0.0507 ns | 0.0543 ns | 0.0508 ns | 0.0000 ns | 0.1548 ns | 1.64 | 2.40 | - | - | NA | +| | | | | | | | | | | | | | +| 'Access BuildConfiguration property' | .NET 9.0 | 0.0260 ns | 0.0296 ns | 0.0277 ns | 0.0210 ns | 0.0000 ns | 0.0874 ns | ? | ? | - | - | ? | +| 'Access IsDebugBuild property' | .NET 9.0 | 0.0312 ns | 0.0285 ns | 0.0292 ns | 0.0331 ns | 0.0000 ns | 0.1032 ns | ? | ? | - | - | ? | +| 'Check assembly debug build status' | .NET 9.0 | 2,434.8425 ns | 197.4947 ns | 219.5148 ns | 2,389.7296 ns | 2,216.3000 ns | 3,040.9758 ns | ? | ? | 0.5510 | 8825 B | ? | +| 'Resolve build configuration from assembly' | .NET 9.0 | 2,256.3108 ns | 122.3223 ns | 130.8835 ns | 2,195.9803 ns | 2,166.4730 ns | 2,592.2347 ns | ? | ? | 0.5484 | 8793 B | ? | +| 'Check entry assembly debug build status' | .NET 9.0 | 2,321.5775 ns | 155.7788 ns | 166.6816 ns | 2,229.0164 ns | 2,149.6204 ns | 2,659.6258 ns | ? | ? | 0.5423 | 8561 B | ? | +| 'Static property access pattern' | .NET 9.0 | 0.2924 ns | 0.0498 ns | 0.0466 ns | 0.2807 ns | 0.2035 ns | 0.3729 ns | ? | ? | - | - | ? | diff --git a/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkWorkerBenchmark-report-github.md b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkWorkerBenchmark-report-github.md new file mode 100644 index 0000000..f02b08d --- /dev/null +++ b/reports/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.BenchmarkWorkerBenchmark-report-github.md @@ -0,0 +1,26 @@ +``` + +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-LDLMHG : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 + Job-IOAYXE : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|---------------------------------- |---------- |--------------:|------------:|------------:|--------------:|--------------:|--------------:|-------:|--------:|-------:|-------:|----------:|------------:| +| 'Construct BenchmarkWorker' | .NET 10.0 | 4.5280 ns | 0.2018 ns | 0.2324 ns | 4.5227 ns | 4.1448 ns | 5.0388 ns | 1.00 | 0.07 | 0.0020 | - | 32 B | 1.00 | +| 'Configure services' | .NET 10.0 | 67.0518 ns | 2.4825 ns | 2.6562 ns | 66.3017 ns | 64.0598 ns | 73.2110 ns | 14.84 | 0.93 | 0.0395 | - | 624 B | 19.50 | +| 'Configure services with options' | .NET 10.0 | 1,310.1326 ns | 35.2009 ns | 39.1257 ns | 1,292.2983 ns | 1,252.4459 ns | 1,383.2906 ns | 290.06 | 16.68 | 0.5072 | 0.0961 | 7976 B | 249.25 | +| 'Access configuration' | .NET 10.0 | 0.6325 ns | 0.0491 ns | 0.0435 ns | 0.6303 ns | 0.5750 ns | 0.7115 ns | 0.14 | 0.01 | - | - | - | 0.00 | +| 'Access environment' | .NET 10.0 | 0.6581 ns | 0.0604 ns | 0.0535 ns | 0.6514 ns | 0.5793 ns | 0.7540 ns | 0.15 | 0.01 | - | - | - | 0.00 | +| | | | | | | | | | | | | | | +| 'Construct BenchmarkWorker' | .NET 9.0 | 4.5442 ns | 0.1618 ns | 0.1799 ns | 4.4841 ns | 4.2818 ns | 4.8308 ns | 1.00 | 0.05 | 0.0020 | - | 32 B | 1.00 | +| 'Configure services' | .NET 9.0 | 78.0880 ns | 1.6229 ns | 1.8038 ns | 78.4799 ns | 75.0539 ns | 81.4879 ns | 17.21 | 0.76 | 0.0396 | - | 624 B | 19.50 | +| 'Configure services with options' | .NET 9.0 | 1,827.5207 ns | 215.5665 ns | 248.2466 ns | 1,916.0051 ns | 1,398.1489 ns | 2,121.1524 ns | 402.76 | 55.60 | 0.5046 | 0.1235 | 7944 B | 248.25 | +| 'Access configuration' | .NET 9.0 | 0.6918 ns | 0.0668 ns | 0.0714 ns | 0.6700 ns | 0.6141 ns | 0.8275 ns | 0.15 | 0.02 | - | - | - | 0.00 | +| 'Access environment' | .NET 9.0 | 0.6554 ns | 0.0526 ns | 0.0492 ns | 0.6580 ns | 0.5846 ns | 0.7320 ns | 0.14 | 0.01 | - | - | - | 0.00 | diff --git a/src/ClassLibrary1/Class1.cs b/src/ClassLibrary1/Class1.cs deleted file mode 100644 index 68fb1be..0000000 --- a/src/ClassLibrary1/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ClassLibrary1 -{ - public class Class1 - { - - } -} diff --git a/src/ClassLibrary1/ClassLibrary1.csproj b/src/ClassLibrary1/ClassLibrary1.csproj deleted file mode 100644 index 88c8dc6..0000000 --- a/src/ClassLibrary1/ClassLibrary1.csproj +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs new file mode 100644 index 0000000..4942782 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkContext.cs @@ -0,0 +1,22 @@ +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +/// +/// Represents the command-line context for a benchmark run. +/// +public class BenchmarkContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The command-line arguments passed to the . + public BenchmarkContext(string[] args) + { + Args = args ?? []; + } + + /// + /// Gets the command-line arguments passed to the . + /// + /// The command-line arguments. + public string[] Args { get; } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs new file mode 100644 index 0000000..dc35e07 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs @@ -0,0 +1,70 @@ +using Codebelt.Bootstrapper.Console; +using Cuemon; +using Cuemon.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console +{ + /// + /// Entry point helper for hosting and running benchmarks using BenchmarkDotNet. + /// + /// + public class BenchmarkProgram : ConsoleProgram + { + static BenchmarkProgram() + { + var isDebugBuild = Decorator.Enclose(Assembly.GetEntryAssembly() ?? 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, setup); + } + + /// + /// Runs benchmarks using a custom implementation of . + /// + /// The type of the workspace that implements . + /// The command-line arguments passed to the application. + /// The which may be configured. + /// + /// This method configures the host builder with the necessary services, builds the host, and runs it to execute benchmarks. + /// + public static void Run(string[] args, Action setup = null) where TWorkspace : class, IBenchmarkWorkspace + { + var hostBuilder = CreateHostBuilder(args); + hostBuilder.ConfigureServices(services => + { + services.AddSingleton(new BenchmarkContext(args)); + services.AddBenchmarkWorkspace(setup); + }); + var host = hostBuilder.Build(); + host.Run(); + } + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs new file mode 100644 index 0000000..e0e0f23 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs @@ -0,0 +1,80 @@ +using BenchmarkDotNet.Running; +using Codebelt.Bootstrapper.Console; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console +{ + /// + /// Worker responsible for executing benchmarks within the console host. + /// + /// + public class BenchmarkWorker : ConsoleStartup + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The host environment. + public BenchmarkWorker(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) + { + } + + /// + /// Configures services for the benchmark runner. Override this method to customize service registration. + /// + /// The service collection to configure. + /// + /// Suppresses console lifetime status messages to keep benchmark output clean. + /// + public override void ConfigureServices(IServiceCollection services) + { + services.Configure(o => o.SuppressStatusMessages = true); + } + + /// + /// Runs the actual benchmarks as envisioned by BenchmarkDotNet. + /// + /// The service provider. + /// The cancellation token. + /// A completed task when benchmark execution has finished. + /// + /// When arguments are provided, they are forwarded to for selective execution. + /// After execution completes, the worker performs artifact post-processing. + /// + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + var options = serviceProvider.GetRequiredService(); + var workspace = serviceProvider.GetRequiredService(); + var assemblies = workspace.LoadBenchmarkAssemblies(); + var context = serviceProvider.GetRequiredService(); + + try + { + if (context.Args.Length == 0) + { + foreach (var assembly in assemblies) + { + BenchmarkRunner.Run(assembly, options.Configuration); + } + } + else + { + BenchmarkSwitcher + .FromAssemblies(assemblies) + .Run(context.Args, options.Configuration); + } + } + finally + { + workspace.PostProcessArtifacts(); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj b/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj new file mode 100644 index 0000000..7e8e79f --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet.Console/Codebelt.Extensions.BenchmarkDotNet.Console.csproj @@ -0,0 +1,16 @@ + + + + The Codebelt.Extensions.BenchmarkDotNet.Console namespace contains types that provide a structured and opinionated console-hosted execution model for BenchmarkDotNet. + benchmark benchmarkdotnet console hosting generic-host dependency-injection performance + + + + + + + + + + + diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs new file mode 100644 index 0000000..d4d0263 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs @@ -0,0 +1,200 @@ +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); + + /// + /// Initializes a new instance of the class with the specified options. + /// + /// The which configures repository paths, build modes and BenchmarkDotNet configuration. + /// + /// cannot be null. + /// + /// + /// are not in a valid state. + /// + public BenchmarkWorkspace(BenchmarkWorkspaceOptions options) + { + Validator.ThrowIfInvalidOptions(options); + if (options.AllowDebugBuild && options.Configuration is ManualConfig mc) + { + mc.Options |= ConfigOptions.DisableOptimizationsValidator; + options.Configuration = mc.AddJob(Job.Default.WithToolchain(new InProcessEmitToolchain(TimeSpan.FromHours(1), true))); + } + _options = options; + } + + /// + /// Loads benchmark assemblies discovered recursively in the tuning folder. + /// + /// An array of instances representing the loaded benchmark assemblies. + /// + /// Assemblies are selected by matching the configured , + /// the build configuration (Debug/Release based on ), + /// and the target framework moniker (). + /// + /// + /// Thrown when no matching assemblies could be loaded from the tuning folder. Ensure the tuning folder contains + /// built benchmark assemblies for the configured build configuration and TFM. + /// + public Assembly[] LoadBenchmarkAssemblies() + { + var useDebugBuild = _options.AllowDebugBuild; + return LoadAssemblies( + _options.RepositoryPath, + _options.TargetFrameworkMoniker, + _options.BenchmarkProjectSuffix, + _options.RepositoryTuningFolder, + useDebugBuild).ToArray(); + } + + /// + /// Performs post-processing of artifacts produced by BenchmarkDotNet. + /// + /// + /// This method moves files found in the BenchmarkDotNet artifacts "results" directory into the tuning folder under the configured artifacts path and then deletes the now-empty "results" directory. + /// + public void PostProcessArtifacts() + { + var reportsResultsPath = Path.Combine(_options.Configuration.ArtifactsPath, "results"); + var reportsTuningPath = Path.Combine(_options.Configuration.ArtifactsPath, _options.RepositoryTuningFolder); + CleanupResults(reportsResultsPath, reportsTuningPath); + } + + private static IEnumerable LoadAssemblies(string repositoryPath, string targetFrameworkMoniker, string benchmarkProjectSuffix, string repositoryTuningFolder, bool useDebugBuild) + { + var tuningDir = Path.Combine(repositoryPath, repositoryTuningFolder); + 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.IndexOf(buildSegment, StringComparison.OrdinalIgnoreCase) >= 0); + + foreach (var path in candidatePaths) + { + try + { + var candidateName = AssemblyName.GetAssemblyName(path); + var existing = alreadyLoaded.FirstOrDefault(a => + AssemblyName.ReferenceMatchesDefinition(a.GetName(), candidateName)); + + 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(s => !s.EndsWith(".lock"))) + { + var targetFile = Path.Combine(reportsTuningPath, Path.GetFileName(file)); + File.Delete(targetFile); + File.Move(file, targetFile); + } + + Directory.Delete(reportsResultsPath, recursive: true); + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs new file mode 100644 index 0000000..3321d0a --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptions.cs @@ -0,0 +1,310 @@ +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Reports; +using Cuemon; +using Cuemon.Configuration; +using Perfolizer.Horology; +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Configuration options for . +/// +/// +/// The following table shows the initial property values for an instance of . +/// +/// +/// Property +/// Initial Value +/// +/// +/// +/// Resolved runtime by filesystem, e.g. C:\Repos\MyBenchmarkRepo. +/// +/// +/// +/// BenchmarkDotNet configured to use recommended settings as outlined in: https://github.com/dotnet/performance/blob/main/src/harness/BenchmarkDotNet.Extensions/RecommendedConfig.cs. +/// +/// +/// +/// Resolved runtime by reflection, e.g. net10.0. +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// false +/// +/// +/// +public class BenchmarkWorkspaceOptions : IValidatableParameterObject, IPostConfigurableParameterObject +{ + /// + /// The default folder name where benchmark reports are written relative to the repository path. + /// + public const string DefaultRepositoryReportsFolder = "reports"; + + /// + /// The default folder name used for tuning artifacts relative to the repository path. + /// + public const string DefaultRepositoryTuningFolder = "tuning"; + + /// + /// The default suffix used to identify benchmark projects. + /// + public const string DefaultBenchmarkProjectSuffix = "Benchmarks"; + + /// + /// A tuned preset that serves as a fast, reliable baseline for most benchmarks, balancing measurement accuracy with developer efficiency. + /// + /// + /// A configured with reduced warmup, shortened iteration duration, controlled iteration counts, and without system power plan enforcement. + /// + /// + /// Based on the recommended configuration used in the .NET Performance repository: https://github.com/dotnet/performance/blob/main/src/harness/BenchmarkDotNet.Extensions/RecommendedConfig.cs + /// + public static readonly Job Slim = GetDefaultConfiguredJob(); + + private static readonly string DefaultRepositoryPath = GetDefaultRepositoryPath(); + private static readonly string DefaultTargetFrameworkMoniker = ResolveCurrentTfm(); + private static readonly CultureInfo DanishCulture = CultureInfo.GetCultureInfo("da-DK"); + + /// + /// Initializes a new instance of the class with sensible defaults. + /// + public BenchmarkWorkspaceOptions() + { + Configuration = GetDefaultConfiguration(); + RepositoryPath = DefaultRepositoryPath; + TargetFrameworkMoniker = DefaultTargetFrameworkMoniker; + RepositoryTuningFolder = DefaultRepositoryTuningFolder; + RepositoryReportsFolder = DefaultRepositoryReportsFolder; + BenchmarkProjectSuffix = DefaultBenchmarkProjectSuffix; + } + + /// + /// Gets or sets the root path of the repository where benchmark projects and reports live. + /// + /// The repository root path. + public string RepositoryPath { get; set; } + + /// + /// Gets or sets the instance used by BenchmarkDotNet. + /// + /// The BenchmarkDotNet configuration. + public IConfig Configuration { get; set; } + + /// + /// Gets or sets the target framework moniker (TFM) string to be used for benchmark discovery and reporting. + /// + /// The target framework moniker to run benchmarks for, e.g. net10.0. + public string TargetFrameworkMoniker { get; set; } + + /// + /// Gets or sets the folder name under used to store tuning artifacts. + /// + /// The tuning folder name. Defaults to . + public string RepositoryTuningFolder { get; set; } + + /// + /// Gets or sets the folder name under used to store generated reports. + /// + /// The reports folder name. Defaults to . + public string RepositoryReportsFolder { get; set; } + + /// + /// Gets or sets the project name suffix used to identify benchmark projects. + /// + /// + /// The benchmark project suffix. Defaults to . + /// + public string BenchmarkProjectSuffix { get; set; } + + /// + /// Gets or sets a value indicating whether the benchmarks should be allowed to run using a Debug build. + /// + /// + /// true to allow Debug builds for benchmarks; otherwise, false. Default is false. + /// + public bool AllowDebugBuild { get; set; } + + /// + /// Finalizes the configured options before use. + /// + /// This method updates the to set the BenchmarkDotNet artifacts path to the combination of and . + public void PostConfigureOptions() + { + if (string.IsNullOrWhiteSpace(Configuration.ArtifactsPath)) + { + var artifactsPath = Path.Combine(RepositoryPath, RepositoryReportsFolder); + + if (Configuration is ManualConfig manual) + { + manual.ArtifactsPath = artifactsPath; + } + else + { + Configuration = Configuration.WithArtifactsPath(artifactsPath); + } + } + } + + /// + /// Determines whether the public read-write properties of this instance are in a valid state. + /// + /// + /// cannot be . + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// -or- + /// cannot be , empty or consist only of white-space characters. + /// + public void ValidateOptions() + { + Validator.ThrowIfInvalidState(Configuration == null); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryPath)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryTuningFolder)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(RepositoryReportsFolder)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(TargetFrameworkMoniker)); + Validator.ThrowIfInvalidState(string.IsNullOrWhiteSpace(BenchmarkProjectSuffix)); + } + + private static string GetDefaultRepositoryPath() + { + var dir = AppContext.BaseDirectory; + while (!string.IsNullOrWhiteSpace(dir)) + { + if (Directory.Exists(Path.Combine(dir, ".git"))) + { + return dir; + } + dir = Path.GetDirectoryName(dir); + } + return Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."); + } + + private static ManualConfig GetDefaultConfiguration() + { + var config = ManualConfig.CreateEmpty() + .WithBuildTimeout(TimeSpan.FromMinutes(15)) // for slow machines + .AddLogger(ConsoleLogger.Default) // log output to console + .AddValidator(DefaultConfig.Instance.GetValidators().ToArray()) // copy default validators + .AddAnalyser(DefaultConfig.Instance.GetAnalysers().ToArray()) // copy default analysers + .AddExporter(MarkdownExporter.GitHub) // export to GitHub markdown + .AddColumnProvider(DefaultColumnProviders.Instance) // display default columns (method name, args etc) + .AddJob(Slim.AsDefault()) // tell BDN that this are our default settings + .AddDiagnoser(MemoryDiagnoser.Default) // MemoryDiagnoser is enabled by default + .AddColumn(StatisticColumn.Median, StatisticColumn.Min, StatisticColumn.Max) + .WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(36).WithCultureInfo(DanishCulture)); // the default is 20 and trims too aggressively some benchmark results + config.Options = ConfigOptions.DisableLogFile; + return config; + } + + private static Job GetDefaultConfiguredJob() + { + return Job.Default + .WithWarmupCount(1) // 1 warmup is enough for our purpose + .WithIterationTime(TimeInterval.FromMilliseconds(250)) // the default is 0.5s per iteration, which is slightly too much for us + .WithMinIterationCount(15) + .WithMaxIterationCount(20) // we don't want to run more than 20 iterations + .DontEnforcePowerPlan(); // make sure BDN does not try to enforce High Performance power plan on Windows + } + + private static string ResolveCurrentTfm() + { + try + { + var entry = Assembly.GetEntryAssembly(); + var tfa = entry?.GetCustomAttribute(); + if (!string.IsNullOrEmpty(tfa?.FrameworkName)) + { + var fn = new FrameworkName(tfa.FrameworkName); + var v = fn.Version; + + // .NET Framework → net11, net20, net35, net40, net403, net45, net451, ..., net48, net481 + if (fn.Identifier.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase)) + { + // Base: net4{minor} or net{major}{minor} for older ones + var tfm = $"net{v.Major}{v.Minor}"; + + // For 4.x: append Build when present (4.0.3 → net403, 4.5.1 → net451, 4.8.1 → net481) + if (v.Major >= 4 && v.Build > 0) + { + tfm += v.Build; + } + + return tfm; + } + + // .NET Standard → netstandard1.0–2.1 + if (fn.Identifier.Equals(".NETStandard", StringComparison.OrdinalIgnoreCase)) + { + return $"netstandard{v.Major}.{v.Minor}"; + } + + // .NET Core / .NET (CoreApp) + if (fn.Identifier.Equals(".NETCoreApp", StringComparison.OrdinalIgnoreCase)) + { + // .NET Core 1.0–3.1 use netcoreappX.Y + if (v.Major <= 3) + { + return $"netcoreapp{v.Major}.{v.Minor}"; + } + + // .NET 5+ uses netX.Y + return $"net{v.Major}.{v.Minor}"; + } + } + } + catch + { + // ignore and fallback to dir inspection + } + + try + { + var baseDir = AppContext.BaseDirectory; + var dir = new DirectoryInfo(baseDir); + while (dir != null) + { + if (dir.Name.StartsWith("net", StringComparison.OrdinalIgnoreCase)) + { + return dir.Name; + } + dir = dir.Parent; + } + } + catch + { + // ignore and fallback to null + } + + return null; + } +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs new file mode 100644 index 0000000..a16659a --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspaceOptionsExtensions.cs @@ -0,0 +1,78 @@ +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/Codebelt.Extensions.BenchmarkDotNet.csproj b/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj new file mode 100644 index 0000000..87bb561 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/Codebelt.Extensions.BenchmarkDotNet.csproj @@ -0,0 +1,13 @@ + + + + The Codebelt.Extensions.BenchmarkDotNet namespace contains types that provide a uniform, opinionated, and extensible way of working with BenchmarkDotNet. + benchmark benchmarkdotnet performance microbenchmark profiling diagnostics + + + + + + + + diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs b/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs new file mode 100644 index 0000000..b340c46 --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/IBenchmarkWorkspace.cs @@ -0,0 +1,20 @@ +using System.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +/// +/// Defines a way for discovering and handling assemblies and their generated artifacts in BenchmarkDotNet. +/// +public interface IBenchmarkWorkspace +{ + /// + /// Loads assemblies that contain BenchmarkDotNet benchmarks. + /// + /// An array of instances representing the loaded benchmark assemblies. + Assembly[] LoadBenchmarkAssemblies(); + + /// + /// Performs post-processing on artifacts produced by BenchmarkDotNet. + /// + void PostProcessArtifacts(); +} diff --git a/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..8030e1f --- /dev/null +++ b/src/Codebelt.Extensions.BenchmarkDotNet/ServiceCollectionExtensions.cs @@ -0,0 +1,45 @@ +using System; +using Cuemon; +using Microsoft.Extensions.DependencyInjection; + +namespace Codebelt.Extensions.BenchmarkDotNet +{ + /// + /// Extension methods for the interface. + /// + public static class ServiceCollectionExtensions + { + /// + /// Adds the default benchmark workspace implementation () to the specified + /// + /// The to add the services to. + /// The which may be configured. + /// The original instance for chaining. + public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection services, Action setup = null) + { + return AddBenchmarkWorkspace(services, setup); + } + + /// + /// Adds a benchmark workspace implementation of type to the specified + /// + /// The type that implements the interface. + /// The to add the services to. + /// The which may be configured. + /// The original instance for chaining. + /// + /// Validates the parameter and the provided configurator. + /// Registers as a singleton using , + /// applies the provided configuration, and registers the resolved as a singleton. + /// + public static IServiceCollection AddBenchmarkWorkspace(this IServiceCollection services, Action setup = null) where 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.Tests/BenchmarkContextTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs new file mode 100644 index 0000000..5df38f3 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkContextTest.cs @@ -0,0 +1,223 @@ +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +public class BenchmarkContextTest : Test +{ + public BenchmarkContextTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldInitializeWithEmptyArray_WhenArgsIsNull() + { + // Act + var context = new BenchmarkContext(null); + + // Assert + Assert.NotNull(context.Args); + Assert.Empty(context.Args); + } + + [Fact] + public void Constructor_ShouldInitializeWithEmptyArray_WhenArgsIsEmpty() + { + // Arrange + var emptyArgs = new string[] { }; + + // Act + var context = new BenchmarkContext(emptyArgs); + + // Assert + Assert.NotNull(context.Args); + Assert.Empty(context.Args); + Assert.Same(emptyArgs, context.Args); + } + + [Fact] + public void Constructor_ShouldInitializeWithProvidedArgs_WhenArgsIsNotEmpty() + { + // Arrange + var args = new[] { "arg1", "arg2", "arg3" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(3, context.Args.Length); + Assert.Same(args, context.Args); + } + + [Fact] + public void Args_ShouldReturnOriginalArray_WhenProvided() + { + // Arrange + var args = new[] { "--filter", "MyBenchmark", "--job", "short" }; + var context = new BenchmarkContext(args); + + // Act + var result = context.Args; + + // Assert + Assert.Same(args, result); + Assert.Equal(4, result.Length); + Assert.Equal("--filter", result[0]); + Assert.Equal("MyBenchmark", result[1]); + Assert.Equal("--job", result[2]); + Assert.Equal("short", result[3]); + } + + [Fact] + public void Args_ShouldBeReadOnly_ButArrayContentsAreMutable() + { + // Arrange + var args = new[] { "original" }; + var context = new BenchmarkContext(args); + + // Act - Modify the original array + args[0] = "modified"; + + // Assert - The property reflects the change (no defensive copy) + Assert.Equal("modified", context.Args[0]); + } + + [Fact] + public void Constructor_ShouldHandleSingleArgument() + { + // Arrange + var args = new[] { "single-arg" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Single(context.Args); + Assert.Equal("single-arg", context.Args[0]); + } + + [Fact] + public void Constructor_ShouldHandleArgsWithSpecialCharacters() + { + // Arrange + var args = new[] { "--config", "Debug", "--output", "C:\\Reports\\benchmark-results.txt" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(4, context.Args.Length); + Assert.Equal("--config", context.Args[0]); + Assert.Equal("Debug", context.Args[1]); + Assert.Equal("--output", context.Args[2]); + Assert.Equal("C:\\Reports\\benchmark-results.txt", context.Args[3]); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + [InlineData(10)] + public void Constructor_ShouldHandleVariousArrayLengths(int length) + { + // Arrange + var args = new string[length]; + for (int i = 0; i < length; i++) + { + args[i] = $"arg{i}"; + } + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(length, context.Args.Length); + + TestOutput.WriteLine($"Created context with {length} arguments"); + } + + [Fact] + public void Constructor_ShouldHandleArgsWithEmptyStrings() + { + // Arrange + var args = new[] { "", "valid", "" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(3, context.Args.Length); + Assert.Equal("", context.Args[0]); + Assert.Equal("valid", context.Args[1]); + Assert.Equal("", context.Args[2]); + } + + [Fact] + public void Constructor_ShouldHandleArgsWithWhitespace() + { + // Arrange + var args = new[] { " ", "valid", "\t\t" }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(3, context.Args.Length); + Assert.Equal(" ", context.Args[0]); + Assert.Equal("valid", context.Args[1]); + Assert.Equal("\t\t", context.Args[2]); + } + + [Fact] + public void Args_ShouldReturnEmptyArray_AfterConstructorWithNull() + { + // Arrange + var context = new BenchmarkContext(null); + + // Act + var args = context.Args; + + // Assert + Assert.NotNull(args); + Assert.Empty(args); + Assert.IsType(args); + } + + [Fact] + public void Constructor_ShouldHandleTypicalBenchmarkDotNetArgs() + { + // Arrange + var args = new[] + { + "--filter", + "Codebelt.Extensions.BenchmarkDotNet.*", + "--job", + "short", + "--exporters", + "markdown", + "--memory" + }; + + // Act + var context = new BenchmarkContext(args); + + // Assert + Assert.NotNull(context.Args); + Assert.Equal(7, context.Args.Length); + Assert.Equal("--filter", context.Args[0]); + Assert.Equal("Codebelt.Extensions.BenchmarkDotNet.*", context.Args[1]); + Assert.Equal("--job", context.Args[2]); + Assert.Equal("short", context.Args[3]); + Assert.Equal("--exporters", context.Args[4]); + Assert.Equal("markdown", context.Args[5]); + Assert.Equal("--memory", context.Args[6]); + + TestOutput.WriteLine($"Successfully created context with {context.Args.Length} BenchmarkDotNet arguments"); + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs new file mode 100644 index 0000000..0e52f8d --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkProgramTest.cs @@ -0,0 +1,402 @@ +using System; +using Codebelt.Extensions.Xunit; +using System.Reflection; +using System.Linq; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +public class BenchmarkProgramTest : Test +{ + public BenchmarkProgramTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void BuildConfiguration_ShouldReturnValidValue() + { + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.NotNull(buildConfiguration); + Assert.True(buildConfiguration == "Debug" || buildConfiguration == "Release"); + + TestOutput.WriteLine($"BuildConfiguration: {buildConfiguration}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeConsistentWithBuildConfiguration() + { + // Act + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + if (buildConfiguration == "Debug") + { + Assert.True(isDebugBuild); + } + else if (buildConfiguration == "Release") + { + Assert.False(isDebugBuild); + } + + TestOutput.WriteLine($"IsDebugBuild: {isDebugBuild}, BuildConfiguration: {buildConfiguration}"); + } + + [Fact] + public void BuildConfiguration_ShouldBeDebug_WhenCompiledInDebugMode() + { + // Arrange + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly == null) + { + // Skip test if entry assembly is not available (e.g., when running in certain test environments) + TestOutput.WriteLine("Entry assembly is null, skipping test"); + return; + } + + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + #if DEBUG + Assert.Equal("Debug", buildConfiguration); + #else + Assert.Equal("Release", buildConfiguration); + #endif + + TestOutput.WriteLine($"BuildConfiguration: {buildConfiguration}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeTrue_WhenCompiledInDebugMode() + { + // Arrange + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly == null) + { + // Skip test if entry assembly is not available + TestOutput.WriteLine("Entry assembly is null, skipping test"); + return; + } + + // Act + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + + // Assert + #if DEBUG + Assert.True(isDebugBuild); + #else + Assert.False(isDebugBuild); + #endif + + TestOutput.WriteLine($"IsDebugBuild: {isDebugBuild}"); + } + + [Fact] + public void BuildConfiguration_ShouldBeCached_AcrossMultipleCalls() + { + // Act + var firstCall = BenchmarkProgram.BuildConfiguration; + var secondCall = BenchmarkProgram.BuildConfiguration; + var thirdCall = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.Equal(firstCall, secondCall); + Assert.Equal(secondCall, thirdCall); + Assert.Same(firstCall, secondCall); // Should be the same string instance + + TestOutput.WriteLine($"BuildConfiguration called 3 times, all returned: {firstCall}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeCached_AcrossMultipleCalls() + { + // Act + var firstCall = BenchmarkProgram.IsDebugBuild; + var secondCall = BenchmarkProgram.IsDebugBuild; + var thirdCall = BenchmarkProgram.IsDebugBuild; + + // Assert + Assert.Equal(firstCall, secondCall); + Assert.Equal(secondCall, thirdCall); + + TestOutput.WriteLine($"IsDebugBuild called 3 times, all returned: {firstCall}"); + } + + [Fact] + public void StaticProperties_ShouldBeInitializedAtStartup() + { + // Act & Assert - Static constructor should have already run + Assert.NotNull(BenchmarkProgram.BuildConfiguration); + Assert.NotEmpty(BenchmarkProgram.BuildConfiguration); + + // IsDebugBuild can be true or false, but should be set + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + Assert.True(isDebugBuild == true || isDebugBuild == false); + + TestOutput.WriteLine($"BuildConfiguration: {BenchmarkProgram.BuildConfiguration}"); + TestOutput.WriteLine($"IsDebugBuild: {BenchmarkProgram.IsDebugBuild}"); + } + + [Theory] + [InlineData("Debug", true)] + [InlineData("Release", false)] + public void BuildConfiguration_ShouldMatchExpectedDebugState(string expectedConfig, bool expectedIsDebug) + { + // Act + var actualConfig = BenchmarkProgram.BuildConfiguration; + var actualIsDebug = BenchmarkProgram.IsDebugBuild; + + // Assert + if (actualConfig == expectedConfig) + { + Assert.Equal(expectedIsDebug, actualIsDebug); + TestOutput.WriteLine($"Configuration '{actualConfig}' correctly maps to IsDebugBuild={actualIsDebug}"); + } + else + { + TestOutput.WriteLine($"Skipping assertion - actual configuration is '{actualConfig}', not '{expectedConfig}'"); + } + } + + [Fact] + public void BuildConfiguration_ShouldNotBeNullOrEmpty() + { + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.NotNull(buildConfiguration); + Assert.NotEmpty(buildConfiguration); + Assert.False(string.IsNullOrWhiteSpace(buildConfiguration)); + + TestOutput.WriteLine($"BuildConfiguration: '{buildConfiguration}'"); + } + + [Fact] + public void BuildConfiguration_ShouldBeOneOfTwoValidValues() + { + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + + // Assert + Assert.Contains(buildConfiguration, new[] { "Debug", "Release" }); + + TestOutput.WriteLine($"BuildConfiguration is valid: {buildConfiguration}"); + } + + [Fact] + public void IsDebugBuild_ShouldBeBoolean() + { + // Act + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + + // Assert + Assert.IsType(isDebugBuild); + + TestOutput.WriteLine($"IsDebugBuild type verified: {isDebugBuild}"); + } + + [Fact] + public void StaticConstructor_ShouldSetPropertiesBasedOnEntryAssembly() + { + // This test verifies that the static constructor logic has executed correctly + // by checking that both properties are set and consistent with each other + + // Act + var buildConfiguration = BenchmarkProgram.BuildConfiguration; + var isDebugBuild = BenchmarkProgram.IsDebugBuild; + + // Assert + Assert.NotNull(buildConfiguration); + + if (isDebugBuild) + { + Assert.Equal("Debug", buildConfiguration); + } + else + { + Assert.Equal("Release", buildConfiguration); + } + + TestOutput.WriteLine($"Static properties correctly initialized - BuildConfiguration: {buildConfiguration}, IsDebugBuild: {isDebugBuild}"); + } + + [Fact] + public void BenchmarkProgram_ShouldInheritFromConsoleProgram() + { + // Act + var baseType = typeof(BenchmarkProgram).BaseType; + + // Assert + Assert.NotNull(baseType); + Assert.True(baseType.IsGenericType); + Assert.Equal("ConsoleProgram`1", baseType.Name); + + TestOutput.WriteLine($"BenchmarkProgram correctly inherits from: {baseType.FullName}"); + } + + [Fact] + public void BenchmarkProgram_ShouldUseCorrectGenericTypeParameter() + { + // Act + var baseType = typeof(BenchmarkProgram).BaseType; + var genericArguments = baseType?.GetGenericArguments(); + + // Assert + Assert.NotNull(genericArguments); + Assert.Single(genericArguments); + Assert.Equal(typeof(BenchmarkWorker), genericArguments[0]); + + TestOutput.WriteLine($"BenchmarkProgram uses correct generic type parameter: {genericArguments[0].Name}"); + } + + [Fact] + public void BenchmarkProgram_ShouldBePublicClass() + { + // Act + var type = typeof(BenchmarkProgram); + + // Assert + Assert.True(type.IsPublic); + Assert.True(type.IsClass); + Assert.False(type.IsAbstract); + Assert.False(type.IsSealed); + + TestOutput.WriteLine($"BenchmarkProgram is a public, non-abstract, non-sealed class"); + } + + [Fact] + public void Run_MethodWithDefaultWorkspace_ShouldExist() + { + // Act + var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + !m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[0].ParameterType == typeof(string[]) && + m.GetParameters()[1].ParameterType == typeof(Action)); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsStatic); + Assert.True(method.IsPublic); + Assert.Equal(typeof(void), method.ReturnType); + + TestOutput.WriteLine($"Found Run method with default workspace: {method}"); + } + + [Fact] + public void Run_GenericMethod_ShouldExist() + { + // Act + var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + m.IsGenericMethodDefinition && + m.GetParameters().Length == 2); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsStatic); + Assert.True(method.IsPublic); + Assert.True(method.IsGenericMethodDefinition); + Assert.Equal(typeof(void), method.ReturnType); + + var genericArguments = method.GetGenericArguments(); + Assert.Single(genericArguments); + + TestOutput.WriteLine($"Found generic Run method: {method}"); + } + + [Fact] + public void Run_GenericMethod_ShouldHaveCorrectConstraints() + { + // Act + var method = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + m.IsGenericMethodDefinition && + m.GetParameters().Length == 2); + + Assert.NotNull(method); + + var genericArguments = method.GetGenericArguments(); + var typeParameter = genericArguments[0]; + + // Assert + var constraints = typeParameter.GetGenericParameterConstraints(); + Assert.Contains(constraints, c => c == typeof(IBenchmarkWorkspace)); + Assert.True(typeParameter.GenericParameterAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint)); + + TestOutput.WriteLine($"Generic type parameter '{typeParameter.Name}' has correct constraints: class, IBenchmarkWorkspace"); + } + + [Fact] + public void StaticProperties_ShouldBeReadOnly() + { + // Act + var buildConfigProperty = typeof(BenchmarkProgram).GetProperty("BuildConfiguration"); + var isDebugBuildProperty = typeof(BenchmarkProgram).GetProperty("IsDebugBuild"); + + // Assert + Assert.NotNull(buildConfigProperty); + Assert.True(buildConfigProperty.CanRead); + Assert.False(buildConfigProperty.CanWrite); + + Assert.NotNull(isDebugBuildProperty); + Assert.True(isDebugBuildProperty.CanRead); + Assert.False(isDebugBuildProperty.CanWrite); + + TestOutput.WriteLine("Both static properties are read-only as expected"); + } + + [Fact] + public void Run_Methods_ShouldHaveCorrectParameterNames() + { + // Act - Get the non-generic Run method explicitly + var nonGenericMethod = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + !m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[0].ParameterType == typeof(string[]) && + m.GetParameters()[1].ParameterType == typeof(Action)); + + // Assert + Assert.NotNull(nonGenericMethod); + + var parameters = nonGenericMethod.GetParameters(); + Assert.Equal(2, parameters.Length); + Assert.Equal("args", parameters[0].Name); + Assert.Equal("setup", parameters[1].Name); + + TestOutput.WriteLine($"Run method parameters: {string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"))}"); + } + + [Fact] + public void Run_Methods_ShouldHaveOptionalSetupParameter() + { + // Act - Get the non-generic Run method explicitly + var nonGenericMethod = typeof(BenchmarkProgram).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => + m.Name == "Run" && + !m.IsGenericMethodDefinition && + m.GetParameters().Length == 2 && + m.GetParameters()[0].ParameterType == typeof(string[]) && + m.GetParameters()[1].ParameterType == typeof(Action)); + + // Assert + Assert.NotNull(nonGenericMethod); + + var setupParameter = nonGenericMethod.GetParameters()[1]; + Assert.True(setupParameter.IsOptional); + Assert.Null(setupParameter.DefaultValue); + + TestOutput.WriteLine($"Setup parameter is optional with default value: {setupParameter.DefaultValue ?? "null"}"); + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs new file mode 100644 index 0000000..9a4f9f4 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/BenchmarkWorkerTest.cs @@ -0,0 +1,539 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +/// +/// Tests for the class. +/// +public class BenchmarkWorkerTest : Test +{ + public BenchmarkWorkerTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldInitialize_WithValidParameters() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + + // Act + var worker = new BenchmarkWorker(configuration, environment); + + // Assert + Assert.NotNull(worker); + } + + [Fact] + public void Constructor_ShouldAcceptNullConfiguration() + { + // Arrange + var environment = CreateMockHostEnvironment(); + + // Act + var worker = new BenchmarkWorker(null, environment); + + // Assert + Assert.NotNull(worker); + } + + [Fact] + public void Constructor_ShouldAcceptNullEnvironment() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + + // Act + var worker = new BenchmarkWorker(configuration, null); + + // Assert + Assert.NotNull(worker); + } + + [Fact] + public void Constructor_ShouldAcceptBothParametersNull() + { + // Act + var worker = new BenchmarkWorker(null, null); + + // Assert + Assert.NotNull(worker); + } + + [Fact] + public void BenchmarkWorker_ShouldInheritFromConsoleStartup() + { + // Act + var baseType = typeof(BenchmarkWorker).BaseType; + + // Assert + Assert.NotNull(baseType); + Assert.Equal("ConsoleStartup", baseType.Name); + + TestOutput.WriteLine($"BenchmarkWorker correctly inherits from: {baseType.FullName}"); + } + + [Fact] + public void BenchmarkWorker_ShouldBePublicClass() + { + // Act + var type = typeof(BenchmarkWorker); + + // Assert + Assert.True(type.IsPublic); + Assert.True(type.IsClass); + Assert.False(type.IsAbstract); + Assert.False(type.IsSealed); + + TestOutput.WriteLine("BenchmarkWorker is a public, non-abstract, non-sealed class"); + } + + [Fact] + public void ConfigureServices_ShouldExist_AndBePublic() + { + // Act + var method = typeof(BenchmarkWorker).GetMethod("ConfigureServices", BindingFlags.Public | BindingFlags.Instance); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsPublic); + Assert.True(method.IsVirtual); + Assert.Equal(typeof(void), method.ReturnType); + + var parameters = method.GetParameters(); + Assert.Single(parameters); + Assert.Equal(typeof(IServiceCollection), parameters[0].ParameterType); + + TestOutput.WriteLine($"Found ConfigureServices method: {method}"); + } + + [Fact] + public void ConfigureServices_ShouldConfigureConsoleLifetimeOptions() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + var services = new ServiceCollection(); + + // Act + worker.ConfigureServices(services); + + // Assert + var serviceDescriptor = services.FirstOrDefault(s => + s.ServiceType.IsGenericType && + s.ServiceType.GetGenericTypeDefinition() == typeof(IConfigureOptions<>) && + s.ServiceType.GetGenericArguments()[0] == typeof(ConsoleLifetimeOptions)); + + Assert.NotNull(serviceDescriptor); + + TestOutput.WriteLine("ConsoleLifetimeOptions configuration was registered"); + } + + [Fact] + public void ConfigureServices_ShouldSuppressStatusMessages() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + var services = new ServiceCollection(); + + // Act + worker.ConfigureServices(services); + + // Build service provider and resolve options + var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetService>()?.Value; + + // Assert + Assert.NotNull(options); + Assert.True(options.SuppressStatusMessages); + + TestOutput.WriteLine($"ConsoleLifetimeOptions.SuppressStatusMessages = {options.SuppressStatusMessages}"); + } + + [Fact] + public void ConfigureServices_ShouldHandleNullServiceCollection() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + // Act & Assert + Assert.Throws(() => worker.ConfigureServices(null)); + } + + [Fact] + public void RunAsync_ShouldExist_AndBePublic() + { + // Act + var method = typeof(BenchmarkWorker).GetMethod("RunAsync", BindingFlags.Public | BindingFlags.Instance); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsPublic); + Assert.True(method.IsVirtual); + Assert.Equal(typeof(Task), method.ReturnType); + + var parameters = method.GetParameters(); + Assert.Equal(2, parameters.Length); + Assert.Equal(typeof(IServiceProvider), parameters[0].ParameterType); + Assert.Equal(typeof(CancellationToken), parameters[1].ParameterType); + + TestOutput.WriteLine($"Found RunAsync method: {method}"); + } + + [Fact] + public async Task RunAsync_ShouldCompleteSuccessfully_WithValidServiceProvider() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var services = CreateServiceProviderWithMocks(); + var cancellationToken = CancellationToken.None; + + // Act + var task = worker.RunAsync(services, cancellationToken); + + // Assert + Assert.NotNull(task); + await task; + Assert.True(task.IsCompletedSuccessfully); + + TestOutput.WriteLine("RunAsync completed successfully"); + } + + [Fact] + public async Task RunAsync_ShouldReturnCompletedTask() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var services = CreateServiceProviderWithMocks(); + var cancellationToken = CancellationToken.None; + + // Act + var task = worker.RunAsync(services, cancellationToken); + + // Assert + await task; + Assert.Equal(TaskStatus.RanToCompletion, task.Status); + Assert.False(task.IsFaulted); + Assert.False(task.IsCanceled); + + TestOutput.WriteLine($"Task status: {task.Status}"); + } + + [Fact] + public async Task RunAsync_ShouldHandleEmptyArgs() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var services = CreateServiceProviderWithMocks(new string[] { }); + var cancellationToken = CancellationToken.None; + + // Act + var task = worker.RunAsync(services, cancellationToken); + + // Assert + await task; + Assert.True(task.IsCompletedSuccessfully); + + TestOutput.WriteLine("RunAsync handled empty args successfully"); + } + + [Fact] + public async Task RunAsync_ShouldHandleArgsWithValues() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var args = new[] { "--filter", "MyBenchmark", "--job", "short" }; + var services = CreateServiceProviderWithMocks(args); + var cancellationToken = CancellationToken.None; + + // Act + var task = worker.RunAsync(services, cancellationToken); + + // Assert + await task; + Assert.True(task.IsCompletedSuccessfully); + + TestOutput.WriteLine($"RunAsync handled args with {args.Length} values successfully"); + } + + [Fact] + public async Task RunAsync_ShouldCallPostProcessArtifacts() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var workspace = new FakeBenchmarkWorkspace(); + var services = CreateServiceProviderWithMocks(workspace: workspace); + var cancellationToken = CancellationToken.None; + + // Act + await worker.RunAsync(services, cancellationToken); + + // Assert + Assert.True(workspace.PostProcessArtifactsCalled); + + TestOutput.WriteLine("PostProcessArtifacts was called"); + } + + [Fact] + public async Task RunAsync_ShouldCallLoadBenchmarkAssemblies() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var workspace = new FakeBenchmarkWorkspace(); + var services = CreateServiceProviderWithMocks(workspace: workspace); + var cancellationToken = CancellationToken.None; + + // Act + await worker.RunAsync(services, cancellationToken); + + // Assert + Assert.True(workspace.LoadBenchmarkAssembliesCalled); + + TestOutput.WriteLine("LoadBenchmarkAssemblies was called"); + } + + [Fact] + public async Task RunAsync_ShouldHandleCancellationToken() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var services = CreateServiceProviderWithMocks(); + var cts = new CancellationTokenSource(); + + // Act + var task = worker.RunAsync(services, cts.Token); + + // Assert + await task; + Assert.True(task.IsCompletedSuccessfully); + + TestOutput.WriteLine("RunAsync handled cancellation token"); + } + + [Fact] + public async Task RunAsync_ShouldCallPostProcessArtifacts_EvenWithEmptyAssemblies() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var workspace = new FakeBenchmarkWorkspace(Array.Empty()); + var services = CreateServiceProviderWithMocks(workspace: workspace); + var cancellationToken = CancellationToken.None; + + // Act + await worker.RunAsync(services, cancellationToken); + + // Assert + Assert.True(workspace.PostProcessArtifactsCalled); + + TestOutput.WriteLine("PostProcessArtifacts was called even with empty assemblies"); + } + + [Fact] + public void Constructor_ShouldHaveCorrectParameterNames() + { + // Act + var constructor = typeof(BenchmarkWorker).GetConstructors().Single(); + var parameters = constructor.GetParameters(); + + // Assert + Assert.Equal(2, parameters.Length); + Assert.Equal("configuration", parameters[0].Name); + Assert.Equal("environment", parameters[1].Name); + + TestOutput.WriteLine($"Constructor parameters: {string.Join(", ", parameters.Select(p => $"{p.ParameterType.Name} {p.Name}"))}"); + } + + [Fact] + public void BenchmarkWorker_ShouldHaveXmlDocumentation() + { + // This test verifies that the class has XML documentation + // by checking for the summary element in the XML doc + + // Act + var type = typeof(BenchmarkWorker); + + // Assert + Assert.NotNull(type); + Assert.True(type.IsPublic); + + TestOutput.WriteLine($"BenchmarkWorker type: {type.FullName}"); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(3)] + [InlineData(5)] + public async Task RunAsync_ShouldHandleVariousArgCounts(int argCount) + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var args = new string[argCount]; + for (int i = 0; i < argCount; i++) + { + args[i] = $"arg{i}"; + } + + var services = CreateServiceProviderWithMocks(args); + var cancellationToken = CancellationToken.None; + + // Act + var task = worker.RunAsync(services, cancellationToken); + + // Assert + await task; + Assert.True(task.IsCompletedSuccessfully); + + TestOutput.WriteLine($"RunAsync handled {argCount} arguments successfully"); + } + + [Fact] + public async Task RunAsync_ShouldUseConfiguration_FromOptions() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var environment = CreateMockHostEnvironment(); + var worker = new BenchmarkWorker(configuration, environment); + + var customConfig = ManualConfig.CreateEmpty(); + var options = new BenchmarkWorkspaceOptions { Configuration = customConfig }; + var services = CreateServiceProviderWithMocks(options: options); + var cancellationToken = CancellationToken.None; + + // Act + await worker.RunAsync(services, cancellationToken); + + // Assert + Assert.True(true); // If we get here without exception, the configuration was used + + TestOutput.WriteLine("RunAsync used configuration from options"); + } + + [Fact] + public void ConfigureServices_ShouldBeOverridable() + { + // Arrange + var method = typeof(BenchmarkWorker).GetMethod("ConfigureServices"); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsVirtual); + Assert.False(method.IsFinal); + + TestOutput.WriteLine("ConfigureServices is virtual and can be overridden"); + } + + [Fact] + public void RunAsync_ShouldBeOverridable() + { + // Arrange + var method = typeof(BenchmarkWorker).GetMethod("RunAsync"); + + // Assert + Assert.NotNull(method); + Assert.True(method.IsVirtual); + Assert.False(method.IsFinal); + + TestOutput.WriteLine("RunAsync is virtual and can be overridden"); + } + + private static IHostEnvironment CreateMockHostEnvironment() + { + var environment = new FakeHostEnvironment + { + EnvironmentName = "Test", + ApplicationName = "BenchmarkWorkerTest", + ContentRootPath = AppContext.BaseDirectory + }; + return environment; + } + + private static IServiceProvider CreateServiceProviderWithMocks(string[] args = null, BenchmarkWorkspaceOptions options = null, FakeBenchmarkWorkspace workspace = null) + { + var services = new ServiceCollection(); + + // Add required services + services.AddSingleton(options ?? new BenchmarkWorkspaceOptions()); + services.AddSingleton(workspace ?? new FakeBenchmarkWorkspace()); + services.AddSingleton(new BenchmarkContext(args ?? Array.Empty())); + + return services.BuildServiceProvider(); + } + + private class FakeHostEnvironment : IHostEnvironment + { + public string EnvironmentName { get; set; } + public string ApplicationName { get; set; } + public string ContentRootPath { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } + } + + private class FakeBenchmarkWorkspace : IBenchmarkWorkspace + { + private readonly Assembly[] _assemblies; + + public FakeBenchmarkWorkspace(Assembly[] assemblies = null) + { + _assemblies = assemblies ?? new[] { typeof(BenchmarkWorkerTest).Assembly }; + } + + public bool LoadBenchmarkAssembliesCalled { get; private set; } + public bool PostProcessArtifactsCalled { get; private set; } + + public Assembly[] LoadBenchmarkAssemblies() + { + LoadBenchmarkAssembliesCalled = true; + return _assemblies; + } + + public void PostProcessArtifacts() + { + PostProcessArtifactsCalled = true; + } + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/Codebelt.Extensions.BenchmarkDotNet.Console.Tests.csproj b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/Codebelt.Extensions.BenchmarkDotNet.Console.Tests.csproj new file mode 100644 index 0000000..62f42db --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Console.Tests/Codebelt.Extensions.BenchmarkDotNet.Console.Tests.csproj @@ -0,0 +1,11 @@ + + + + Codebelt.Extensions.BenchmarkDotNet.Console + + + + + + + diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs new file mode 100644 index 0000000..afa9657 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceOptionsTest.cs @@ -0,0 +1,649 @@ +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.Xunit; +using System; +using System.IO; +using System.Linq; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +public class BenchmarkWorkspaceOptionsTest : Test +{ + public BenchmarkWorkspaceOptionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldInitializeWithDefaultValues() + { + // Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.RepositoryPath); + Assert.NotNull(options.Configuration); + Assert.NotNull(options.TargetFrameworkMoniker); + Assert.Equal(BenchmarkWorkspaceOptions.DefaultRepositoryTuningFolder, options.RepositoryTuningFolder); + Assert.Equal(BenchmarkWorkspaceOptions.DefaultRepositoryReportsFolder, options.RepositoryReportsFolder); + Assert.Equal(BenchmarkWorkspaceOptions.DefaultBenchmarkProjectSuffix, options.BenchmarkProjectSuffix); + Assert.False(options.AllowDebugBuild); + } + + [Fact] + public void DefaultRepositoryReportsFolder_ShouldBeReports() + { + // Assert + Assert.Equal("reports", BenchmarkWorkspaceOptions.DefaultRepositoryReportsFolder); + } + + [Fact] + public void DefaultRepositoryTuningFolder_ShouldBeTuning() + { + // Assert + Assert.Equal("tuning", BenchmarkWorkspaceOptions.DefaultRepositoryTuningFolder); + } + + [Fact] + public void DefaultBenchmarkProjectSuffix_ShouldBeBenchmarks() + { + // Assert + Assert.Equal("Benchmarks", BenchmarkWorkspaceOptions.DefaultBenchmarkProjectSuffix); + } + + [Fact] + public void RepositoryPath_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testPath = @"C:\TestRepo"; + + // Act + options.RepositoryPath = testPath; + + // Assert + Assert.Equal(testPath, options.RepositoryPath); + } + + [Fact] + public void Configuration_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var customConfig = ManualConfig.CreateEmpty(); + + // Act + options.Configuration = customConfig; + + // Assert + Assert.Same(customConfig, options.Configuration); + } + + [Fact] + public void TargetFrameworkMoniker_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testTfm = "net8.0"; + + // Act + options.TargetFrameworkMoniker = testTfm; + + // Assert + Assert.Equal(testTfm, options.TargetFrameworkMoniker); + } + + [Fact] + public void RepositoryTuningFolder_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testFolder = "custom-tuning"; + + // Act + options.RepositoryTuningFolder = testFolder; + + // Assert + Assert.Equal(testFolder, options.RepositoryTuningFolder); + } + + [Fact] + public void RepositoryReportsFolder_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testFolder = "custom-reports"; + + // Act + options.RepositoryReportsFolder = testFolder; + + // Assert + Assert.Equal(testFolder, options.RepositoryReportsFolder); + } + + [Fact] + public void BenchmarkProjectSuffix_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testSuffix = "Perf"; + + // Act + options.BenchmarkProjectSuffix = testSuffix; + + // Assert + Assert.Equal(testSuffix, options.BenchmarkProjectSuffix); + } + + [Fact] + public void UseDebugBuild_ShouldBeSettable() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act + options.AllowDebugBuild = true; + + // Assert + Assert.True(options.AllowDebugBuild); + } + + [Fact] + public void PostConfigureOptions_ShouldCombineRepositoryPathAndReportsFolder() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + var testRepoPath = @"C:\MyBenchmarks"; + var testReportsFolder = "output"; + options.RepositoryPath = testRepoPath; + options.RepositoryReportsFolder = testReportsFolder; + + // Act + options.PostConfigureOptions(); + + // Assert + var expectedPath = Path.Combine(testRepoPath, testReportsFolder); + Assert.Equal(expectedPath, options.Configuration.ArtifactsPath); + } + + [Fact] + public void ValidateOptions_ShouldNotThrow_WhenAllPropertiesAreValid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act & Assert + var exception = Record.Exception(() => options.ValidateOptions()); + Assert.Null(exception); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenConfigurationIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + Configuration = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryPathIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryTuningFolder = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryTuningFolder = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryTuningFolderIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryTuningFolder = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryReportsFolder = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryReportsFolder = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenRepositoryReportsFolderIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryReportsFolder = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + TargetFrameworkMoniker = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + TargetFrameworkMoniker = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenTargetFrameworkMonikerIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + TargetFrameworkMoniker = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsNull() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = null + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsEmpty() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = string.Empty + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void ValidateOptions_ShouldThrow_WhenBenchmarkProjectSuffixIsWhitespace() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = " " + }; + + // Act & Assert + Assert.Throws(() => options.ValidateOptions()); + } + + [Fact] + public void Constructor_ShouldSetRepositoryPath_ToGitRootOrFallback() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.RepositoryPath); + 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_ShouldHaveDanishCultureInfo() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotNull(options.Configuration.SummaryStyle); + Assert.NotNull(options.Configuration.SummaryStyle.CultureInfo); + Assert.Equal("da-DK", options.Configuration.SummaryStyle.CultureInfo.Name); + + TestOutput.WriteLine($"CultureInfo: {options.Configuration.SummaryStyle.CultureInfo.Name}"); + } + + [Fact] + public void Configuration_ShouldHaveDisabledLogFile() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.True((options.Configuration.Options & ConfigOptions.DisableLogFile) == ConfigOptions.DisableLogFile); + } + + [Fact] + public void Configuration_ShouldHaveBuildTimeout() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.Equal(TimeSpan.FromMinutes(15), options.Configuration.BuildTimeout); + + TestOutput.WriteLine($"BuildTimeout: {options.Configuration.BuildTimeout}"); + } + + [Fact] + public void Configuration_ShouldHaveDefaultValidatorsAndAnalysers() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + Assert.NotEmpty(options.Configuration.GetValidators()); + Assert.NotEmpty(options.Configuration.GetAnalysers()); + + TestOutput.WriteLine($"Validators: {options.Configuration.GetValidators().Count()}"); + TestOutput.WriteLine($"Analysers: {options.Configuration.GetAnalysers().Count()}"); + } + + [Fact] + public void Configuration_ShouldHaveSlimJobAsDefault() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + + // Assert + var jobs = options.Configuration.GetJobs().ToList(); + Assert.Single(jobs); + Assert.True(jobs[0].Meta.IsDefault); + + TestOutput.WriteLine($"Default Job - WarmupCount: {jobs[0].Run.WarmupCount}"); + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs new file mode 100644 index 0000000..9f07e74 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/BenchmarkWorkspaceTest.cs @@ -0,0 +1,630 @@ +using BenchmarkDotNet.Configs; +using Codebelt.Extensions.Xunit; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Cuemon; +using Cuemon.Reflection; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +public class BenchmarkWorkspaceTest : Test +{ + private static readonly bool IsDebugBuild = GetBuildConfiguration(); + + public BenchmarkWorkspaceTest(ITestOutputHelper output) : base(output) + { + } + + private static bool GetBuildConfiguration() + { + return Decorator.Enclose(typeof(BenchmarkWorkspaceTest).Assembly).IsDebugBuild(); + } + + [Fact] + public void Constructor_ShouldThrowArgumentNullException_WhenOptionsIsNull() + { + // Arrange + BenchmarkWorkspaceOptions options = null; + + // Act & Assert + Assert.Throws(() => new BenchmarkWorkspace(options)); + } + + [Fact] + public void Constructor_ShouldThrowArgumentException_WhenOptionsAreInvalid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = null + }; + + // Act & Assert + Assert.Throws(() => new BenchmarkWorkspace(options)); + } + + [Fact] + public void Constructor_ShouldSucceed_WhenOptionsAreValid() + { + // Arrange + var options = new BenchmarkWorkspaceOptions(); + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + } + + [Fact] + public void Constructor_ShouldEnableDisableOptimizationsValidator_WhenUseDebugBuildIsTrue() + { + // Arrange + var config = ManualConfig.CreateEmpty(); + var options = new BenchmarkWorkspaceOptions + { + Configuration = config, + AllowDebugBuild = true + }; + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + var manualConfig = options.Configuration as ManualConfig; + Assert.NotNull(manualConfig); + Assert.True(manualConfig.Options.HasFlag(ConfigOptions.DisableOptimizationsValidator)); + } + + [Fact] + public void Constructor_ShouldNotModifyConfiguration_WhenUseDebugBuildIsFalse() + { + // Arrange + var config = ManualConfig.CreateEmpty(); + var originalOptions = config.Options; + var options = new BenchmarkWorkspaceOptions + { + Configuration = config, + AllowDebugBuild = false + }; + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + var manualConfig = options.Configuration as ManualConfig; + Assert.NotNull(manualConfig); + Assert.Equal(originalOptions, manualConfig.Options); + } + + [Fact] + public void Constructor_ShouldNotModifyConfiguration_WhenConfigurationIsNotManualConfig() + { + // Arrange + var config = DefaultConfig.Instance; + var options = new BenchmarkWorkspaceOptions + { + Configuration = config + }; + + // Act + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.NotNull(workspace); + Assert.Same(config, options.Configuration); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldThrowInvalidOperationException_WhenNoAssembliesFound() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act & Assert + var exception = Assert.Throws(() => workspace.LoadBenchmarkAssemblies()); + + TestOutput.WriteLine($"{exception.Message}"); + + Assert.Contains("No assemblies were loaded", exception.Message); + Assert.Contains(IsDebugBuild ? "Debug" : "Release", exception.Message); + Assert.Contains("net10.0", exception.Message); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldCreateTuningDirectory_WhenItDoesNotExist() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + + var workspace = new BenchmarkWorkspace(options); + var expectedTuningPath = Path.Combine(tempPath, "tuning"); + + // Act + try + { + workspace.LoadBenchmarkAssemblies(); + } + catch (InvalidOperationException) + { + // Expected when no assemblies found + } + + // Assert + Assert.True(Directory.Exists(expectedTuningPath)); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldReturnLoadedAssemblies_WhenMatchingAssembliesExist() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.NotEmpty(assemblies); + Assert.All(assemblies, assembly => Assert.NotNull(assembly)); + + TestOutput.WriteLine($"Current build configuration: {(IsDebugBuild ? "Debug" : "Release")}"); + TestOutput.WriteLine($"Loaded {assemblies.Length} benchmark assemblies:"); + foreach (var assembly in assemblies) + { + TestOutput.WriteLine($" - {assembly.GetName().Name}"); + } + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldFilterByBuildConfiguration() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + var expectedBuildConfig = IsDebugBuild ? "Debug" : "Release"; + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.All(assemblies, assembly => + { + var location = assembly.Location; + TestOutput.WriteLine($"Assembly location: {location}"); + Assert.Contains(expectedBuildConfig, location, StringComparison.OrdinalIgnoreCase); + }); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldFilterByTargetFrameworkMoniker() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + var expectedTfm = options.TargetFrameworkMoniker; + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.All(assemblies, assembly => + { + var location = assembly.Location; + TestOutput.WriteLine($"Assembly location: {location}"); + Assert.Contains(expectedTfm, location, StringComparison.OrdinalIgnoreCase); + }); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldFilterByBenchmarkProjectSuffix() + { + // Arrange + var options = new BenchmarkWorkspaceOptions + { + BenchmarkProjectSuffix = "Benchmarks", + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.All(assemblies, assembly => + { + var name = assembly.GetName().Name; + TestOutput.WriteLine($"Assembly name: {name}"); + Assert.Contains("Benchmarks", name, StringComparison.OrdinalIgnoreCase); + }); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldNotLoadDuplicateAssemblies() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + var uniqueAssemblies = assemblies.Distinct().ToArray(); + Assert.Equal(assemblies.Length, uniqueAssemblies.Length); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldReuseAlreadyLoadedAssemblies() + { + // Arrange + var options = new BenchmarkWorkspaceOptions() + { + AllowDebugBuild = IsDebugBuild, + TargetFrameworkMoniker = "net10.0" + }; + var workspace = new BenchmarkWorkspace(options); + + // Act + var firstLoad = workspace.LoadBenchmarkAssemblies(); + var secondLoad = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(firstLoad); + Assert.NotNull(secondLoad); + Assert.All(firstLoad, firstAssembly => + { + var matchingSecondAssembly = secondLoad.FirstOrDefault(second => + AssemblyName.ReferenceMatchesDefinition(second.GetName(), firstAssembly.GetName())); + Assert.NotNull(matchingSecondAssembly); + }); + } + + [Fact] + public void PostProcessArtifacts_ShouldMoveFilesFromResultsDirectory() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + + Directory.CreateDirectory(resultsDir); + + var testFile1 = Path.Combine(resultsDir, "test1.txt"); + var testFile2 = Path.Combine(resultsDir, "test2.md"); + File.WriteAllText(testFile1, "Test content 1"); + File.WriteAllText(testFile2, "Test content 2"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.True(Directory.Exists(tuningDir)); + Assert.True(File.Exists(Path.Combine(tuningDir, "test1.txt"))); + Assert.True(File.Exists(Path.Combine(tuningDir, "test2.md"))); + + TestOutput.WriteLine($"Files moved successfully to: {tuningDir}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldDoNothing_WhenResultsDirectoryDoesNotExist() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + Directory.CreateDirectory(artifactsPath); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + var exception = Record.Exception(() => workspace.PostProcessArtifacts()); + + // Assert + Assert.Null(exception); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldOverwriteExistingFiles() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var tuningDir = Path.Combine(artifactsPath, "tuning"); + + Directory.CreateDirectory(resultsDir); + Directory.CreateDirectory(tuningDir); + + var sourceFile = Path.Combine(resultsDir, "test.txt"); + var targetFile = Path.Combine(tuningDir, "test.txt"); + + File.WriteAllText(sourceFile, "New content"); + File.WriteAllText(targetFile, "Old content"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.True(File.Exists(targetFile)); + var content = File.ReadAllText(targetFile); + Assert.Equal("New content", content); + + TestOutput.WriteLine($"File successfully overwritten at: {targetFile}"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldDeleteResultsDirectoryRecursively() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + var subDir = Path.Combine(resultsDir, "subdir"); + + Directory.CreateDirectory(subDir); + File.WriteAllText(Path.Combine(resultsDir, "file1.txt"), "Content 1"); + File.WriteAllText(Path.Combine(subDir, "file2.txt"), "Content 2"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.False(Directory.Exists(resultsDir)); + Assert.False(Directory.Exists(subDir)); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void PostProcessArtifacts_ShouldCreateTuningDirectory_WhenItDoesNotExist() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(tempPath); + var artifactsPath = Path.Combine(tempPath, "artifacts"); + var resultsDir = Path.Combine(artifactsPath, "results"); + + Directory.CreateDirectory(resultsDir); + File.WriteAllText(Path.Combine(resultsDir, "test.txt"), "Content"); + + var config = ManualConfig.CreateEmpty().WithArtifactsPath(artifactsPath); + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + Configuration = config, + RepositoryTuningFolder = "tuning" + }; + + var workspace = new BenchmarkWorkspace(options); + var expectedTuningPath = Path.Combine(artifactsPath, "tuning"); + + // Act + workspace.PostProcessArtifacts(); + + // Assert + Assert.True(Directory.Exists(expectedTuningPath)); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } + + [Fact] + public void BenchmarkWorkspace_ShouldImplementIBenchmarkWorkspace() + { + // Arrange & Act + var options = new BenchmarkWorkspaceOptions(); + var workspace = new BenchmarkWorkspace(options); + + // Assert + Assert.IsAssignableFrom(workspace); + } + + [Fact] + public void LoadBenchmarkAssemblies_ShouldHandleAssemblyLoadFailuresGracefully() + { + // Arrange + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var buildConfig = IsDebugBuild ? "Debug" : "Release"; + try + { + Directory.CreateDirectory(tempPath); + var tuningDir = Path.Combine(tempPath, "tuning"); + var buildDir = Path.Combine(tuningDir, "bin", buildConfig, "net10.0"); + Directory.CreateDirectory(buildDir); + + // Create an invalid DLL file + var invalidDll = Path.Combine(buildDir, "Invalid.Benchmarks.dll"); + File.WriteAllText(invalidDll, "This is not a valid assembly"); + + // Create a valid assembly reference + var validDll = Path.Combine(buildDir, "Valid.Benchmarks.dll"); + File.Copy(Assembly.GetExecutingAssembly().Location, validDll, true); + + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = tempPath, + RepositoryTuningFolder = "tuning", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = IsDebugBuild + }; + + var workspace = new BenchmarkWorkspace(options); + + // Act + var assemblies = workspace.LoadBenchmarkAssemblies(); + + // Assert + Assert.NotNull(assemblies); + Assert.NotEmpty(assemblies); + Assert.All(assemblies, assembly => Assert.NotNull(assembly)); + + TestOutput.WriteLine($"Build configuration: {buildConfig}"); + TestOutput.WriteLine($"Successfully loaded {assemblies.Length} valid assemblies while skipping invalid ones"); + } + finally + { + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + } + } +} diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj new file mode 100644 index 0000000..24cde66 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/Codebelt.Extensions.BenchmarkDotNet.Tests.csproj @@ -0,0 +1,11 @@ + + + + Codebelt.Extensions.BenchmarkDotNet + + + + + + + diff --git a/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs new file mode 100644 index 0000000..86907e9 --- /dev/null +++ b/test/Codebelt.Extensions.BenchmarkDotNet.Tests/ServiceCollectionExtensionsTest.cs @@ -0,0 +1,56 @@ +using System; +using System.Reflection; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Codebelt.Extensions.BenchmarkDotNet +{ + public class ServiceCollectionExtensionsTest : Test + { + public ServiceCollectionExtensionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void AddBenchmarkWorkspace_ShouldThrowWhenServicesIsNull() + { + Assert.Throws(() => ServiceCollectionExtensions.AddBenchmarkWorkspace((IServiceCollection)null)); + } + + [Fact] + public void AddBenchmarkWorkspace_ShouldRegisterDefaultWorkspaceAndOptions_WhenCalledWithoutGeneric() + { + var services = new ServiceCollection(); + services.AddBenchmarkWorkspace(setup: options => options.BenchmarkProjectSuffix = "MySuffix"); + using var sp = services.BuildServiceProvider(); + + var workspace = sp.GetRequiredService(); + Assert.IsType(workspace); + + var options = sp.GetRequiredService(); + Assert.Equal("MySuffix", options.BenchmarkProjectSuffix); + } + + [Fact] + public void AddBenchmarkWorkspace_GenericOverload_ShouldRegisterCustomImplementationAndOptions() + { + var services = new ServiceCollection(); + services.AddBenchmarkWorkspace(setup: options => options.RepositoryPath = "repo-path"); + using var sp = services.BuildServiceProvider(); + + var workspace = sp.GetRequiredService(); + Assert.IsType(workspace); + + var options = sp.GetRequiredService(); + Assert.Equal("repo-path", options.RepositoryPath); + } + + private sealed class FakeWorkspace : IBenchmarkWorkspace + { + public Assembly[] LoadBenchmarkAssemblies() => Array.Empty(); + + public void PostProcessArtifacts() { } + } + } +} diff --git a/test/TestProject1.FunctionalTests/Class1Test.cs b/test/TestProject1.FunctionalTests/Class1Test.cs deleted file mode 100644 index 2ec9e90..0000000 --- a/test/TestProject1.FunctionalTests/Class1Test.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Codebelt.Extensions.Xunit; - -namespace ClassLibrary1 -{ - public class Class1Test : Test - { - - } -} diff --git a/test/TestProject1.FunctionalTests/TestProject1.FunctionalTests.csproj b/test/TestProject1.FunctionalTests/TestProject1.FunctionalTests.csproj deleted file mode 100644 index 7bb358a..0000000 --- a/test/TestProject1.FunctionalTests/TestProject1.FunctionalTests.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - Classlibrary1 - - - - - - - diff --git a/test/TestProject1/Class1Test.cs b/test/TestProject1/Class1Test.cs deleted file mode 100644 index 9790743..0000000 --- a/test/TestProject1/Class1Test.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Codebelt.Extensions.Xunit; -using Xunit; - -namespace Classlibrary1 -{ - public class Class1Test : Test - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/test/TestProject1/TestProject1.Tests.csproj b/test/TestProject1/TestProject1.Tests.csproj deleted file mode 100644 index 7bb358a..0000000 --- a/test/TestProject1/TestProject1.Tests.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - Classlibrary1 - - - - - - - diff --git a/testenvironments.json b/testenvironments.json index 2bc34cb..9807401 100644 --- a/testenvironments.json +++ b/testenvironments.json @@ -9,7 +9,7 @@ { "name": "Docker-Ubuntu", "type": "docker", - "dockerImage": "gimlichael/ubuntu-testrunner:net8.0.416-9.0.307-10.0.100" + "dockerImage": "gimlichael/ubuntu-testrunner:net8.0.416-9.0.307-10.0.101" } ] } diff --git a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj new file mode 100644 index 0000000..4f5d4af --- /dev/null +++ b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Codebelt.Extensions.BenchmarkDotNet.Runner.csproj @@ -0,0 +1,25 @@ + + + + Exe + net10.0 + dotnet-bdn-runner + $(MSBuildProjectName) + + + + + + + + + false + + + + + + + + + diff --git a/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs new file mode 100644 index 0000000..ef47eb4 --- /dev/null +++ b/tooling/Codebelt.Extensions.BenchmarkDotNet.Runner/Program.cs @@ -0,0 +1,24 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using Codebelt.Extensions.BenchmarkDotNet.Console; + +namespace Codebelt.Extensions.BenchmarkDotNet.Runner +{ + public class Program + { + public static void Main(string[] args) + { + BenchmarkProgram.Run(args, o => + { + o.AllowDebugBuild = BenchmarkProgram.IsDebugBuild; + o.ConfigureBenchmarkDotNet(c => + { + var slimJob = BenchmarkWorkspaceOptions.Slim; + return c.AddJob(slimJob.WithRuntime(CoreRuntime.Core90)) + .AddJob(slimJob.WithRuntime(CoreRuntime.Core10_0)); + }); + }); + } + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs new file mode 100644 index 0000000..0371c76 --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceBenchmark.cs @@ -0,0 +1,72 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using System; +using System.IO; +using System.Reflection; +using Cuemon; +using Cuemon.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkWorkspaceBenchmark +{ + private BenchmarkWorkspace _workspace; + + [GlobalSetup] + public void GlobalSetup() + { + var isDebugBuild = Decorator.Enclose(GetType().Assembly).IsDebugBuild(); + var options = new BenchmarkWorkspaceOptions() + { + RepositoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()), + AllowDebugBuild = isDebugBuild + }; + _workspace = new BenchmarkWorkspace(options); + + var buildConfig = isDebugBuild + ? "Debug" + : "Release"; + var tuningDir = Path.Combine(options.RepositoryPath, "tuning"); + + CreateBenchmarkAssembly(tuningDir, buildConfig, "net10.0", "Valid.Benchmarks.dll"); + CreateBenchmarkAssembly(tuningDir, buildConfig, "net9.0", "Valid.Benchmarks.dll"); + } + + [Benchmark(Baseline = true, Description = "Construct BenchmarkDotNetWorkspace")] + public void ConstructWorkspaceBenchmark() + { + var v = new BenchmarkWorkspace(new BenchmarkWorkspaceOptions()); + } + + [Benchmark(Description = "Load assemblies from tuning folder (no matching assemblies)")] + public void LoadBenchmarkAssembliesBenchmark() + { + // In the prepared environment there are no matching *.Benchmarks.dll files, + // so LoadBenchmarkAssemblies will exercise the enumeration/path logic without loading assemblies. + var result = _workspace.LoadBenchmarkAssemblies(); + GC.KeepAlive(result); + } + + [Benchmark(Description = "PostProcessArtifacts (move results -> tuning folder)")] + public void PostProcessArtifactsBenchmark() + { + _workspace.PostProcessArtifacts(); + } + + /// + /// Creates a benchmark assembly by copying the executing assembly to a target framework-specific build directory. + /// + /// The base tuning directory path. + /// The build configuration (e.g., "Debug" or "Release"). + /// The target framework moniker (e.g., "net9.0", "net10.0"). + /// The name of the DLL file to create. + private void CreateBenchmarkAssembly(string tuningDir, string buildConfig, string targetFramework, string dllName) + { + var buildDir = Path.Combine(tuningDir, "bin", buildConfig, targetFramework); + Directory.CreateDirectory(buildDir); + var targetDll = Path.Combine(buildDir, dllName); + File.Copy(Assembly.GetExecutingAssembly().Location, targetDll, true); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs new file mode 100644 index 0000000..0006210 --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/BenchmarkWorkspaceOptionsBenchmark.cs @@ -0,0 +1,114 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using System; + +namespace Codebelt.Extensions.BenchmarkDotNet; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkWorkspaceOptionsBenchmark +{ + private BenchmarkWorkspaceOptions _options; + + [GlobalSetup] + public void Setup() + { + // Pre-allocate an instance for benchmarks that need existing state + _options = new BenchmarkWorkspaceOptions(); + } + + [Benchmark(Baseline = true, Description = "Create default BenchmarkWorkspaceOptions")] + [BenchmarkCategory("Construction")] + public BenchmarkWorkspaceOptions CreateDefaultOptions() + { + return new BenchmarkWorkspaceOptions(); + } + + [Benchmark(Description = "Create and configure BenchmarkWorkspaceOptions")] + [BenchmarkCategory("Construction")] + public BenchmarkWorkspaceOptions CreateAndConfigureOptions() + { + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = AppContext.BaseDirectory, + RepositoryTuningFolder = "tuning", + RepositoryReportsFolder = "reports", + BenchmarkProjectSuffix = "Benchmarks", + TargetFrameworkMoniker = "net9.0", + AllowDebugBuild = false + }; + return options; + } + + [Benchmark(Description = "ValidateOptions - valid state")] + [BenchmarkCategory("Validation")] + public void ValidateOptions_ValidState() + { + _options.ValidateOptions(); + } + + [Benchmark(Description = "PostConfigureOptions - default config")] + [BenchmarkCategory("Configuration")] + public void PostConfigureOptions_DefaultConfig() + { + var options = new BenchmarkWorkspaceOptions(); + options.PostConfigureOptions(); + GC.KeepAlive(options); + } + + [Benchmark(Description = "PostConfigureOptions - custom config")] + [BenchmarkCategory("Configuration")] + public void PostConfigureOptions_CustomConfig() + { + var options = new BenchmarkWorkspaceOptions + { + Configuration = ManualConfig.CreateEmpty() + }; + options.PostConfigureOptions(); + GC.KeepAlive(options); + } + + [Benchmark(Description = "Property access - RepositoryPath")] + [BenchmarkCategory("PropertyAccess")] + public string AccessRepositoryPath() + { + return _options.RepositoryPath; + } + + [Benchmark(Description = "Property access - Configuration")] + [BenchmarkCategory("PropertyAccess")] + public IConfig AccessConfiguration() + { + return _options.Configuration; + } + + [Benchmark(Description = "Property modification - set all properties")] + [BenchmarkCategory("PropertyModification")] + public void SetAllProperties() + { + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = AppContext.BaseDirectory, + RepositoryTuningFolder = "custom-tuning", + RepositoryReportsFolder = "custom-reports", + BenchmarkProjectSuffix = "Perf", + TargetFrameworkMoniker = "net10.0", + AllowDebugBuild = true + }; + GC.KeepAlive(options); + } + + [Benchmark(Description = "Full lifecycle - create, configure, validate")] + [BenchmarkCategory("Lifecycle")] + public void FullLifecycle() + { + var options = new BenchmarkWorkspaceOptions + { + RepositoryPath = AppContext.BaseDirectory, + TargetFrameworkMoniker = "net9.0" + }; + options.PostConfigureOptions(); + options.ValidateOptions(); + GC.KeepAlive(options); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj new file mode 100644 index 0000000..24cde66 --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Benchmarks.csproj @@ -0,0 +1,11 @@ + + + + Codebelt.Extensions.BenchmarkDotNet + + + + + + + diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs new file mode 100644 index 0000000..06b45fc --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkContextBenchmark.cs @@ -0,0 +1,74 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkContextBenchmark +{ + private string[] _emptyArgs; + private string[] _smallArgs; + private string[] _mediumArgs; + private string[] _largeArgs; + + [Params(0, 8, 64, 256)] + public int ArgsCount { get; set; } + + [GlobalSetup] + public void Setup() + { + // Deterministic initialization of test data + _emptyArgs = []; + + _smallArgs = new string[8]; + for (int i = 0; i < _smallArgs.Length; i++) + { + _smallArgs[i] = $"--arg{i}"; + } + + _mediumArgs = new string[64]; + for (int i = 0; i < _mediumArgs.Length; i++) + { + _mediumArgs[i] = $"--option{i}"; + } + + _largeArgs = new string[256]; + for (int i = 0; i < _largeArgs.Length; i++) + { + _largeArgs[i] = $"--parameter{i}=value{i}"; + } + } + + [Benchmark(Baseline = true, Description = "Construct BenchmarkContext with empty args")] + public BenchmarkContext ConstructWithEmptyArgs() + { + return new BenchmarkContext(_emptyArgs); + } + + [Benchmark(Description = "Construct BenchmarkContext with null args")] + public BenchmarkContext ConstructWithNullArgs() + { + return new BenchmarkContext(null); + } + + [Benchmark(Description = "Construct BenchmarkContext with varied args count")] + public BenchmarkContext ConstructWithVariedArgs() + { + return ArgsCount switch + { + 0 => new BenchmarkContext(_emptyArgs), + 8 => new BenchmarkContext(_smallArgs), + 64 => new BenchmarkContext(_mediumArgs), + 256 => new BenchmarkContext(_largeArgs), + _ => new BenchmarkContext(_emptyArgs) + }; + } + + [Benchmark(Description = "Access Args property")] + public string[] AccessArgsProperty() + { + var context = new BenchmarkContext(_smallArgs); + return context.Args; + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs new file mode 100644 index 0000000..de17aeb --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkProgramBenchmark.cs @@ -0,0 +1,61 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using Cuemon; +using Cuemon.Reflection; +using System; +using System.Reflection; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkProgramBenchmark +{ + private Assembly _testAssembly; + private Assembly _entryAssembly; + + [GlobalSetup] + public void Setup() + { + // Deterministic initialization of test data + _testAssembly = Assembly.GetExecutingAssembly(); + _entryAssembly = Assembly.GetEntryAssembly() ?? _testAssembly; + } + + [Benchmark(Baseline = true, Description = "Access BuildConfiguration property")] + public string AccessBuildConfiguration() + { + return BenchmarkProgram.BuildConfiguration; + } + + [Benchmark(Description = "Access IsDebugBuild property")] + public bool AccessIsDebugBuild() + { + return BenchmarkProgram.IsDebugBuild; + } + + [Benchmark(Description = "Check assembly debug build status")] + public bool CheckAssemblyDebugBuild() + { + return Decorator.Enclose(_testAssembly).IsDebugBuild(); + } + + [Benchmark(Description = "Resolve build configuration from assembly")] + public string ResolveBuildConfiguration() + { + var isDebugBuild = Decorator.Enclose(_testAssembly).IsDebugBuild(); + return isDebugBuild ? "Debug" : "Release"; + } + + [Benchmark(Description = "Check entry assembly debug build status")] + public bool CheckEntryAssemblyDebugBuild() + { + return Decorator.Enclose(_entryAssembly).IsDebugBuild(); + } + + [Benchmark(Description = "Static property access pattern")] + public (string, bool) AccessStaticProperties() + { + return (BenchmarkProgram.BuildConfiguration, BenchmarkProgram.IsDebugBuild); + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkWorkerBenchmark.cs b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkWorkerBenchmark.cs new file mode 100644 index 0000000..6c0e214 --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/BenchmarkWorkerBenchmark.cs @@ -0,0 +1,92 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using System.Collections.Generic; + +namespace Codebelt.Extensions.BenchmarkDotNet.Console; + +/// +/// Benchmarks for the class. +/// +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public class BenchmarkWorkerBenchmark +{ + private IConfiguration _configuration; + private IHostEnvironment _environment; + private BenchmarkWorker _worker; + private IServiceCollection _services; + + [GlobalSetup] + public void Setup() + { + // Deterministic initialization of test data + var configData = new Dictionary + { + ["Setting1"] = "Value1", + ["Setting2"] = "Value2" + }; + + _configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configData) + .Build(); + + _environment = new TestHostEnvironment + { + EnvironmentName = "Development", + ApplicationName = "BenchmarkTest" + }; + + _worker = new BenchmarkWorker(_configuration, _environment); + _services = new ServiceCollection(); + } + + [Benchmark(Baseline = true, Description = "Construct BenchmarkWorker")] + public BenchmarkWorker ConstructWorker() + { + return new BenchmarkWorker(_configuration, _environment); + } + + [Benchmark(Description = "Configure services")] + public IServiceCollection ConfigureServices() + { + var services = new ServiceCollection(); + _worker.ConfigureServices(services); + return services; + } + + [Benchmark(Description = "Configure services with options")] + public IServiceCollection ConfigureServicesWithOptions() + { + var services = new ServiceCollection(); + _worker.ConfigureServices(services); + var provider = services.BuildServiceProvider(); + return services; + } + + [Benchmark(Description = "Access configuration")] + public IConfiguration AccessConfiguration() + { + return _configuration; + } + + [Benchmark(Description = "Access environment")] + public IHostEnvironment AccessEnvironment() + { + return _environment; + } + + /// + /// Test implementation of IHostEnvironment for benchmark purposes. + /// + private class TestHostEnvironment : IHostEnvironment + { + public string EnvironmentName { get; set; } + public string ApplicationName { get; set; } + public string ContentRootPath { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } + } +} diff --git a/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj new file mode 100644 index 0000000..ec5743f --- /dev/null +++ b/tuning/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks/Codebelt.Extensions.BenchmarkDotNet.Console.Benchmarks.csproj @@ -0,0 +1,11 @@ + + + + Codebelt.Extensions.BenchmarkDotNet.Console + + + + + + +