diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
new file mode 100644
index 000000000..e55976fe0
--- /dev/null
+++ b/.github/workflows/benchmark.yml
@@ -0,0 +1,61 @@
+name: Coverlet Benchmarks
+
+# Description:
+# This workflow automates performance benchmarking for the Coverlet project.
+#
+# Triggers:
+# - Manual trigger via workflow_dispatch
+#
+# What it does:
+# 1. Sets up windows environment
+# 2. Installs .NET SDK based on global.json
+# 3. Builds benchmark tests in Release mode
+# 4. Runs performance benchmarks using BenchmarkDotNet
+# 5. Uploads benchmark results as artifacts
+#
+# Results:
+# - Benchmark artifacts are stored under: artifacts/bin/coverlet.core.benchmark.tests/release/BenchmarkDotNet.Artifacts/results/
+# - Results can be downloaded from GitHub Actions artifacts
+#
+# Usage:
+# - Can be manually triggered from Actions tab using workflow_dispatch
+# - Use results to monitor performance changes over time
+
+
+on:
+ workflow_dispatch: # Manual trigger
+ push:
+ branches:
+ - master
+ paths-ignore:
+ - '**.md'
+ - '.gitignore'
+ - '.editorconfig'
+
+jobs:
+ benchmark:
+ name: Run Benchmarks
+ runs-on: windows-latest # Using Windows as per the benchmark results context
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ global-json-file: ./global.json
+
+ - name: Build Benchmark Project
+ run: dotnet build test/coverlet.core.benchmark.tests -c Release
+
+ - name: Run Benchmarks
+ working-directory: artifacts/bin/coverlet.core.benchmark.tests/release
+ run: ./coverlet.core.benchmark.tests.exe
+
+ - name: Upload Benchmark Results
+ uses: actions/upload-artifact@v4
+ with:
+ name: benchmark-results
+ path: |
+ artifacts/bin/coverlet.core.benchmark.tests/release/BenchmarkDotNet.Artifacts/**/*
+ if-no-files-found: error
diff --git a/.gitignore b/.gitignore
index 514880624..d53610b92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -315,6 +315,8 @@ coverage.*.cobertura.xml
coverage.*.opencover.xml
FolderProfile.pubxml
+BenchmarkDotNet.Artifacts/
/NuGet.config
nuget.config
*.dmp
+test/coverlet.core.benchmark.tests/InstrumentationHelperBenchmarks.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index eba94e254..46a7505a5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,7 +9,7 @@
17.13.9
- 4.12.0
+ 4.13.0
17.13.0
6.13.2
@@ -17,6 +17,10 @@
3.0.2
+
+
+
+
@@ -54,18 +58,18 @@
-
+
-
+
-
+
diff --git a/coverlet.sln b/coverlet.sln
index a449bd072..6dd67136f 100644
--- a/coverlet.sln
+++ b/coverlet.sln
@@ -86,6 +86,8 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "coverlet.tests.projectsampl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.tests.utils", "test\coverlet.tests.utils\coverlet.tests.utils.csproj", "{0B109210-03CB-413F-888C-3023994AA384}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.core.benchmark.tests", "test\coverlet.core.benchmark.tests\coverlet.core.benchmark.tests.csproj", "{8E8C4799-6F9D-49D8-96EA-E9BD1D187DAD}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.tests.projectsample.wpf8.selfcontained", "test\coverlet.tests.projectsample.wpf8.selfcontained\coverlet.tests.projectsample.wpf8.selfcontained.csproj", "{71004336-9896-4AE5-8367-B29BB1680542}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "coverlet.core.coverage.tests", "test\coverlet.core.coverage.tests\coverlet.core.coverage.tests.csproj", "{F74AD549-EFE0-4CD9-AD10-B2189E3FD5BB}"
@@ -188,6 +190,10 @@ Global
{0B109210-03CB-413F-888C-3023994AA384}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B109210-03CB-413F-888C-3023994AA384}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B109210-03CB-413F-888C-3023994AA384}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8E8C4799-6F9D-49D8-96EA-E9BD1D187DAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8E8C4799-6F9D-49D8-96EA-E9BD1D187DAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8E8C4799-6F9D-49D8-96EA-E9BD1D187DAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8E8C4799-6F9D-49D8-96EA-E9BD1D187DAD}.Release|Any CPU.Build.0 = Release|Any CPU
{71004336-9896-4AE5-8367-B29BB1680542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71004336-9896-4AE5-8367-B29BB1680542}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71004336-9896-4AE5-8367-B29BB1680542}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -225,6 +231,7 @@ Global
{351A034E-E642-4DB9-A21D-F71C8151C243} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{03400776-1F9A-4326-B927-1CA9B64B42A1} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{0B109210-03CB-413F-888C-3023994AA384} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
+ {8E8C4799-6F9D-49D8-96EA-E9BD1D187DAD} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{71004336-9896-4AE5-8367-B29BB1680542} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
{F74AD549-EFE0-4CD9-AD10-B2189E3FD5BB} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134}
EndGlobalSection
diff --git a/global.json b/global.json
index 6dfc6666e..8b2877a60 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,5 @@
{
"sdk": {
- "version": "8.0.407"
+ "version": "8.0.409"
}
}
diff --git a/src/coverlet.core/Properties/AssemblyInfo.cs b/src/coverlet.core/Properties/AssemblyInfo.cs
index 0a6d02544..3e03b0a2f 100644
--- a/src/coverlet.core/Properties/AssemblyInfo.cs
+++ b/src/coverlet.core/Properties/AssemblyInfo.cs
@@ -12,6 +12,7 @@
[assembly: InternalsVisibleTo("coverlet.core.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a82e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e86c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722aead64ad")]
[assembly: InternalsVisibleTo("coverlet.core.coverage.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100094aad8eb75c06c9f2443dda84573b8db55cd6678452a60010db2643467ac28928db3a06b0b1ac3016645b448937d5e671b36504bcfc0fda27e996c5e1b0ee49747145cda6d47508d1e3c60b144634d95e33d4efe49536372df8139f48d3d897ae6931c2876d4f5d00215fd991cbcecde2705e53e19309e21c8b59d19eb925b1")]
+[assembly: InternalsVisibleTo("coverlet.core.benchmark.tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010061d9d48f9cd6a4733ea1d88bc8a09c53a3040c3446c41858781df135170e8fe4e82a6cc6d9836f070ae0a28ebd7cd6e30dc1a853b350ae08ae77f437bc9f9f3b0ef23eb9b05eea38f97edb26a2dd2d0d8b32c6335c47b32f5277621118267f1a5717233eae25a3fe126d89d14b85a7a8e07657bf681a8a82100762a42ec477aa")]
[assembly: InternalsVisibleTo("coverlet.collector.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100ed0ed6af9693182615b8dcadc83c918b8d36312f86cefc69539d67d4189cd1b89420e7c3871802ffef7f5ca7816c68ad856c77bf7c230cc07824d96aa5d1237eebd30e246b9a14e22695fb26b40c800f74ea96619092cbd3a5d430d6c003fc7a82e8ccd1e315b935105d9232fe9e99e8d7ff54bba6f191959338d4a3169df9b3")]
[assembly: InternalsVisibleTo("coverlet.msbuild.tasks.tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010071b1583d63637a225f3f640252fee7130f0f3f2127d75025c1c3ee2d6dfc79a4950919268e0784d7ff54b0eadd8e4762e3e150da422e20e091eb0811d9d84e1779d5b95e349d5428aebb16e82e081bdf805926c5a9eb2094aaed9d36442de024264976a8835c7d6923047cf2f745e8f0ded2332f8980acd390f725224d976ed8")]
[assembly: InternalsVisibleTo("coverlet.integration.tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010001d24efbe9cbc2dc49b7a3d2ae34ca37cfb69b4f450acd768a22ce5cd021c8a38ae7dc68b2809a1ac606ad531b578f192a5690b2986990cbda4dd84ec65a3a4c1c36f6d7bb18f08592b93091535eaee2f0c8e48763ed7f190db2008e1f9e0facd5c0df5aaab74febd3430e09a428a72e5e6b88357f92d78e47512d46ebdc3cbb")]
diff --git a/test/coverlet.core.benchmark.tests/CoverageBenchmarks.cs b/test/coverlet.core.benchmark.tests/CoverageBenchmarks.cs
new file mode 100644
index 000000000..4018da93e
--- /dev/null
+++ b/test/coverlet.core.benchmark.tests/CoverageBenchmarks.cs
@@ -0,0 +1,68 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using BenchmarkDotNet.Attributes;
+using Coverlet.Core;
+using Coverlet.Core.Abstractions;
+using Coverlet.Core.Helpers;
+using Coverlet.Core.Symbols;
+using Moq;
+
+namespace coverlet.core.benchmark.tests
+{
+ [MemoryDiagnoser]
+ public class CoverageBenchmarks
+ {
+ private Coverage _coverage;
+ private readonly Mock _mockLogger = new();
+ private DirectoryInfo _directory;
+
+ [GlobalSetup(Target = nameof(GetCoverageBenchmark))]
+ public void GetCoverageBenchmarkSetup()
+ {
+ string module = GetType().Assembly.Location;
+ string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb");
+
+ _directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
+
+ File.Copy(module, Path.Combine(_directory.FullName, Path.GetFileName(module)), true);
+ File.Copy(pdb, Path.Combine(_directory.FullName, Path.GetFileName(pdb)), true);
+
+ // TODO: Find a way to mimick hits
+ var instrumentationHelper =
+ new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object,
+ new SourceRootTranslator(module, new Mock().Object, new FileSystem(), new AssemblyAdapter()));
+
+ var parameters = new CoverageParameters
+ {
+ IncludeFilters = new string[] { "[coverlet.tests.projectsample.excludedbyattribute*]*" },
+ IncludeDirectories = Array.Empty(),
+ ExcludeFilters = Array.Empty(),
+ ExcludedSourceFiles = Array.Empty(),
+ ExcludeAttributes = Array.Empty(),
+ IncludeTestAssembly = false,
+ SingleHit = false,
+ MergeWith = string.Empty,
+ UseSourceLink = false
+ };
+
+ _coverage = new Coverage(Path.Combine(_directory.FullName, Path.GetFileName(module)), parameters, _mockLogger.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(_mockLogger.Object, new FileSystem()), new CecilSymbolHelper());
+ _coverage.PrepareModules();
+
+ }
+
+ [GlobalCleanup]
+ public void IterationCleanup()
+ {
+ _directory.Delete(true);
+ }
+
+ [Benchmark]
+ public void GetCoverageBenchmark()
+ {
+ CoverageResult result = _coverage.GetCoverageResult();
+ }
+ }
+}
diff --git a/test/coverlet.core.benchmark.tests/HowTo.md b/test/coverlet.core.benchmark.tests/HowTo.md
new file mode 100644
index 000000000..17ee0acaf
--- /dev/null
+++ b/test/coverlet.core.benchmark.tests/HowTo.md
@@ -0,0 +1,134 @@
+# How to benchmark coverlet.core
+
+Coverlet.core.benchmark uses [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) which has some runtime requirements
+
+- Build the project in `Release` mode
+- Make sure you have the latest version of the .NET SDK installed
+- Make sure you have the latest version of the BenchmarkDotNet package installed
+
+Use a terminal and run the following commands:
+
+```bash
+dotnet build test/coverlet.core.benchmark.tests -c release
+cd artifacts/bin/coverlet.core.benchmark.tests/release
+./coverlet.core.benchmark.tests.exe
+```
+
+> [!TIP]
+> If error occurred missing `TestAssets\System.Private.CoreLib.dll` or `TestAssets\System.Private.CoreLib.pdb`.
+> Just copy the files from `artifacts\bin\coverlet.core.tests\debug\TestAssets`.
+
+The benchmark will automatically create reports in folder `BenchmarkDotNet.Artifacts` eg. find these files:
+
+```text
+BenchmarkRun-20250411-083105.log
+results\BenchmarkRun-joined-2025-04-11-08-38-13-report-github.md
+results\BenchmarkRun-joined-2025-04-11-08-38-13-report.csv
+results\BenchmarkRun-joined-2025-04-11-08-38-13-report.html
+results\BenchmarkRun-joined-2025-04-11-08-55-34-report-github.md
+results\BenchmarkRun-joined-2025-04-11-08-55-34-report.csv
+results\BenchmarkRun-joined-2025-04-11-08-55-34-report.html
+```
+
+> [!NOTE]
+> This should be done for every coverlet release to avoid performance degradations.
+
+## Additional information
+
+- [BenchmarkDotNet](https://benchmarkdotnet.org)
+- [Analyze BenchmarkDotNet data in Visual Studio](https://learn.microsoft.com/en-us/visualstudio/profiling/profiling-with-benchmark-dotnet)
+- [.NET benchmarking and profiling for beginners](https://medium.com/ingeniouslysimple/net-benchmarking-and-profiling-for-beginners-62462e1e9a19)
+
+
+
+## Coverlet 6.0.0
+
+```text
+BenchmarkDotNet v0.14.0, Windows 11 (10.0.26120.3671)
+AMD Ryzen 7 Microsoft Surface Edition, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 9.0.203
+ [Host] : .NET 6.0.36 (6.0.3624.51421), X64 RyuJIT AVX2
+
+Job=ShortRun Toolchain=InProcessNoEmitToolchain IterationCount=3
+LaunchCount=1 WarmupCount=3
+
+```
+
+| Type | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
+|----------------------- |---------------------- |--------------------:|-------------------:|------------------:|------------:|------------:|----------:|-------------:|
+| CoverageBenchmarks | GetCoverageBenchmark | 46.42 ns | 1.670 ns | 0.092 ns | 0.0612 | - | - | 128 B |
+| InstrumenterBenchmarks | InstrumenterBenchmark | 4,938,713,766.67 ns | 767,760,955.034 ns | 42,083,568.809 ns | 857000.0000 | 109000.0000 | 2000.0000 | 2879633880 B |
+
+## Coverlet 6.0.1
+
+```text
+BenchmarkDotNet v0.14.0, Windows 11 (10.0.26120.3671)
+AMD Ryzen 7 Microsoft Surface Edition, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 8.0.408
+ [Host] : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2
+
+Job=ShortRun Toolchain=InProcessNoEmitToolchain IterationCount=3
+LaunchCount=1 WarmupCount=3
+
+```
+
+| Type | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
+|----------------------- |---------------------- |--------------------:|-------------------:|------------------:|------------:|-----------:|----------:|-------------:|
+| CoverageBenchmarks | GetCoverageBenchmark | 48.14 ns | 8.681 ns | 0.476 ns | 0.0612 | - | - | 128 B |
+| InstrumenterBenchmarks | InstrumenterBenchmark | 3,675,771,933.33 ns | 874,256,026.013 ns | 47,920,923.025 ns | 789000.0000 | 97000.0000 | 2000.0000 | 2864466608 B |
+
+## Coverlet 6.0.2
+
+```text
+BenchmarkDotNet v0.14.0, Windows 11 (10.0.26120.3671)
+AMD Ryzen 7 Microsoft Surface Edition, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 8.0.408
+ [Host] : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2
+
+Job=ShortRun Toolchain=InProcessNoEmitToolchain IterationCount=3
+LaunchCount=1 WarmupCount=3
+
+```
+
+| Type | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
+|----------------------- |---------------------- |---------------------:|--------------------:|-------------------:|------------:|------------:|----------:|-------------:|
+| CoverageBenchmarks | GetCoverageBenchmark | 46.20 ns | 12.15 ns | 0.666 ns | 0.0612 | - | - | 128 B |
+| InstrumenterBenchmarks | InstrumenterBenchmark | 19,105,224,033.33 ns | 4,450,103,671.99 ns | 243,925,199.451 ns | 867000.0000 | 130000.0000 | 2000.0000 | 3170097400 B |
+
+## Coverlet 6.0.3
+
+```text
+BenchmarkDotNet v0.14.0, Windows 11 (10.0.26120.3671)
+AMD Ryzen 7 Microsoft Surface Edition, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 8.0.408
+ [Host] : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2
+
+Job=ShortRun Toolchain=InProcessNoEmitToolchain IterationCount=3
+LaunchCount=1 WarmupCount=3
+
+```
+
+| Type | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
+|----------------------- |---------------------- |--------------------:|-------------------:|------------------:|------------:|-----------:|----------:|-------------:|
+| CoverageBenchmarks | GetCoverageBenchmark | 47.32 ns | 4.246 ns | 0.233 ns | 0.0612 | - | - | 128 B |
+| InstrumenterBenchmarks | InstrumenterBenchmark | 3,620,665,600.00 ns | 580,611,812.738 ns | 31,825,292.772 ns | 775000.0000 | 91000.0000 | 2000.0000 | 2798558288 B |
+
+## Coverlet 6.0.4
+
+```text
+BenchmarkDotNet v0.14.0, Windows 11 (10.0.26120.3671)
+AMD Ryzen 7 Microsoft Surface Edition, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 8.0.408
+ [Host] : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2
+
+Job=ShortRun Toolchain=InProcessNoEmitToolchain IterationCount=3
+LaunchCount=1 WarmupCount=3
+
+```
+
+| Type | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
+|----------------------- |---------------------- |--------------------:|-------------------:|------------------:|------------:|-----------:|----------:|-------------:|
+| CoverageBenchmarks | GetCoverageBenchmark | 43.59 ns | 9.890 ns | 0.542 ns | 0.0612 | - | - | 128 B |
+| InstrumenterBenchmarks | InstrumenterBenchmark | 3,594,263,533.33 ns | 193,126,202.387 ns | 10,585,898.871 ns | 776000.0000 | 97000.0000 | 2000.0000 | 2798557560 B |
+
+
diff --git a/test/coverlet.core.benchmark.tests/InstrumenterBenchmarks.cs b/test/coverlet.core.benchmark.tests/InstrumenterBenchmarks.cs
new file mode 100644
index 000000000..d28bdd894
--- /dev/null
+++ b/test/coverlet.core.benchmark.tests/InstrumenterBenchmarks.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using BenchmarkDotNet.Attributes;
+using Coverlet.Core;
+using Coverlet.Core.Abstractions;
+using Coverlet.Core.Helpers;
+using Coverlet.Core.Instrumentation;
+using Coverlet.Core.Symbols;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace coverlet.core.benchmark.tests
+{
+ public class InstrumenterBenchmarks
+ {
+ private Coverlet.Core.Abstractions.ILogger _logger;
+ private IFileSystem _fileSystem;
+ private Instrumenter _instrumenter;
+ private Coverage _coverage;
+ private CoverageParameters _coverageParameters;
+ private CoveragePrepareResult _coveragePrepareResult;
+ private ISourceRootTranslator _sourceRootTranslator;
+ private CoverageParameters _parameters;
+ private IInstrumentationHelper _instrumentationHelper;
+ private ICecilSymbolHelper _cecilSymbolHelper;
+
+ [GlobalCleanup]
+ public void IterationCleanup()
+ {
+
+ }
+
+ [Benchmark]
+ public void InstrumenterBigClassBenchmark()
+ {
+ string testSubjectDLLFilePath = Path.Combine(Directory.GetCurrentDirectory(), "coverlet.testsubject.dll");
+ string _coverletTestSubjectArtifactPath = Directory.GetCurrentDirectory();
+ _logger = new ConsoleLogger();
+
+ _coverageParameters = new CoverageParameters
+ {
+ Module = testSubjectDLLFilePath,
+ IncludeFilters = ["[coverlet.testsubject]*"],
+ IncludeDirectories = [_coverletTestSubjectArtifactPath],
+ ExcludeFilters = null,
+ ExcludedSourceFiles = null,
+ ExcludeAttributes = null,
+ IncludeTestAssembly = true,
+ SingleHit = false,
+ MergeWith = string.Empty,
+ UseSourceLink = false,
+ SkipAutoProps = true,
+ DeterministicReport = false,
+ ExcludeAssembliesWithoutSources = "None",
+ };
+
+ // Set up service collection like in InstrumentationTask
+ IServiceCollection serviceCollection = new ServiceCollection();
+ // These can stay transient
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient(_ => _logger);
+
+ // Make all symbol and instrumentation related services singletons
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton(provider => new SourceRootTranslator("", provider.GetRequiredService(), provider.GetRequiredService()));
+
+ serviceCollection.AddSingleton(serviceProvider =>
+ new InstrumentationHelper(
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService()));
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ // Initialize helpers using the service provider
+ _fileSystem = serviceProvider.GetRequiredService();
+ _instrumentationHelper = serviceProvider.GetRequiredService();
+ _sourceRootTranslator = serviceProvider.GetRequiredService();
+ _cecilSymbolHelper = serviceProvider.GetRequiredService();
+
+ _sourceRootTranslator = new SourceRootTranslator(_logger, new FileSystem());
+ _parameters = new CoverageParameters();
+ _instrumentationHelper =
+ new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), _fileSystem, _logger, _sourceRootTranslator);
+ _instrumenter = new Instrumenter(testSubjectDLLFilePath, "_coverlet_instrumented", _parameters, _logger, _instrumentationHelper, _fileSystem, _sourceRootTranslator, new CecilSymbolHelper());
+
+ _coverage = new Coverage(
+ testSubjectDLLFilePath,
+ _coverageParameters,
+ _logger,
+ _instrumentationHelper,
+ _fileSystem,
+ _sourceRootTranslator,
+ _cecilSymbolHelper
+ );
+
+ // Prepare modules for instrumentation
+ _coveragePrepareResult = _coverage.PrepareModules();
+
+ if (_coveragePrepareResult.Results.Length == 0)
+ {
+ throw (new InvalidOperationException("Instrumentation failed: _coveragePrepareResult.Results missing"));
+ }
+
+ }
+ }
+}
diff --git a/test/coverlet.core.benchmark.tests/Program.cs b/test/coverlet.core.benchmark.tests/Program.cs
new file mode 100644
index 000000000..dc487f23c
--- /dev/null
+++ b/test/coverlet.core.benchmark.tests/Program.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+//using System.Diagnostics.Tracing;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+//using BenchmarkDotNet.Diagnostics.Windows;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Exporters.Csv;
+using BenchmarkDotNet.Exporters.Json;
+using BenchmarkDotNet.Exporters.Xml;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Running;
+using BenchmarkDotNet.Toolchains.InProcess.NoEmit;
+//using Microsoft.Diagnostics.NETCore.Client;
+//using Microsoft.Diagnostics.Tracing.Parsers;
+
+namespace coverlet.core.benchmark.tests
+{
+ public class Program
+ {
+
+ public static void Main(string[] args)
+ {
+ var config = DefaultConfig.Instance
+ .WithOptions(ConfigOptions.DisableOptimizationsValidator)
+ .WithOptions(ConfigOptions.JoinSummary)
+ .WithOption(ConfigOptions.DisableLogFile, true)
+ .AddJob(Job
+ .ShortRun
+ .WithLaunchCount(1)
+ .WithToolchain(InProcessNoEmitToolchain.Instance))
+ .AddExporter(CsvExporter.Default, CsvMeasurementsExporter.Default, RPlotExporter.Default, HtmlExporter.Default, JsonExporter.Default, MarkdownExporter.GitHub, XmlExporter.Default)
+ .AddDiagnoser(MemoryDiagnoser.Default, ThreadingDiagnoser.Default, ExceptionDiagnoser.Default)
+ //.AddDiagnoser(new InliningDiagnoser(), new EtwProfiler()) // only windows platform, requires elevated privileges
+ //.AddDiagnoser(new EventPipeProfiler(EventPipeProfile.CpuSampling)) // stops collecting results ???
+ ;
+#if DEBUG
+ config = config.WithOptions(ConfigOptions.DisableOptimizationsValidator);
+ System.Diagnostics.Debugger.Launch(); // Optional: force debugger attachment
+#endif
+ var summary = BenchmarkRunner.Run(new[]{
+ BenchmarkConverter.TypeToBenchmarks( typeof(CoverageBenchmarks), config),
+ BenchmarkConverter.TypeToBenchmarks( typeof(InstrumenterBenchmarks), config),
+ BenchmarkConverter.TypeToBenchmarks( typeof(CoverageWorkflowBenchmark ), config),
+ });
+
+ // Use this to select benchmarks from the console and execute with additional options e.g. 'coverlet.core.benchmark.tests.exe --profiler EP'
+ //var summaries = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
+ }
+ }
+}
diff --git a/test/coverlet.core.benchmark.tests/Properties/AssemblyInfo.cs b/test/coverlet.core.benchmark.tests/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..0bd3d9a20
--- /dev/null
+++ b/test/coverlet.core.benchmark.tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Reflection;
+
+[assembly: AssemblyKeyFile("coverlet.core.benchmark.tests.snk")]
diff --git a/test/coverlet.core.benchmark.tests/Simulator.cs b/test/coverlet.core.benchmark.tests/Simulator.cs
new file mode 100644
index 000000000..275ed4b27
--- /dev/null
+++ b/test/coverlet.core.benchmark.tests/Simulator.cs
@@ -0,0 +1,640 @@
+// Copyright (c) Toni Solarin-Sodara
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using ConsoleTables;
+using Coverlet.Core;
+using Coverlet.Core.Abstractions;
+using Coverlet.Core.Helpers;
+using Coverlet.Core.Instrumentation;
+using Coverlet.Core.Reporters;
+using Coverlet.Core.Symbols;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace coverlet.core.benchmark.tests
+{
+ [SimpleJob(launchCount: 1, warmupCount: 0, iterationCount: 3)]
+ public class CoverageWorkflowBenchmark
+ {
+ //private string _tempRoot;
+ //private string _isolatedAssemblyPath;
+ private Coverage _coverage;
+ private CoveragePrepareResult _coveragePrepareResult;
+ private CoverageParameters _coverageParameters;
+ private IInstrumentationHelper _instrumentationHelper;
+ private ICecilSymbolHelper _cecilSymbolHelper;
+ private Coverlet.Core.Abstractions.ILogger _logger;
+ private IFileSystem _fileSystem;
+ private string _coverletTestSubjectSourcePath;
+ private string _coverletTestSubjectDllPath;
+ private string _coverletTestSubjectArtifactPath;
+ private string _rId;
+ private ISourceRootTranslator _sourceRootTranslator;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _logger = new ConsoleLogger();
+
+ // Get benchmark directory
+ string benchmarkDir = AppContext.BaseDirectory;
+ _logger.LogInformation($"benchmark path: {benchmarkDir}");
+
+ // Find solution root by locating Directory.Build.props
+ string currentPath = benchmarkDir;
+ string solutionRoot = null;
+ while (currentPath != null)
+ {
+ string buildPropsPath = Path.Combine(currentPath, "Directory.Build.props");
+ if (File.Exists(buildPropsPath))
+ {
+ solutionRoot = currentPath;
+ break;
+ }
+ currentPath = Path.GetDirectoryName(currentPath);
+ }
+
+ if (solutionRoot == null)
+ {
+ throw new DirectoryNotFoundException("Could not find solution root containing Directory.Build.props");
+ }
+
+ _logger.LogInformation($"Solution root path: {solutionRoot}");
+ // Build source location path
+ _coverletTestSubjectSourcePath = Path.GetFullPath(Path.Combine(solutionRoot, "test", "coverlet.testsubject"));
+
+ _logger.LogInformation($"Source path: {_coverletTestSubjectSourcePath}");
+
+ if (!Directory.Exists(_coverletTestSubjectSourcePath))
+ {
+ throw new DirectoryNotFoundException($"Source directory not found: {_coverletTestSubjectSourcePath}");
+ }
+
+ _rId = GetPortableRuntimeIdentifier();
+
+ StopBuildServer();
+
+ // Set up service collection like in InstrumentationTask
+ IServiceCollection serviceCollection = new ServiceCollection();
+
+ // These can stay transient
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient(_ => _logger);
+
+ // Make all symbol and instrumentation related services singletons
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton(provider => new SourceRootTranslator("", provider.GetRequiredService(), provider.GetRequiredService()));
+
+ serviceCollection.AddSingleton(serviceProvider =>
+ new InstrumentationHelper(
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService(),
+ serviceProvider.GetRequiredService()));
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ // Initialize helpers using the service provider
+ _fileSystem = serviceProvider.GetRequiredService();
+ _instrumentationHelper = serviceProvider.GetRequiredService();
+ _sourceRootTranslator = serviceProvider.GetRequiredService();
+ _cecilSymbolHelper = serviceProvider.GetRequiredService();
+ }
+
+ private void StopBuildServer()
+ {
+ Process.RunToCompletion(
+ DotnetMuxer.Path.FullName,
+ $"build-server shutdown",
+ workingDirectory: _coverletTestSubjectSourcePath);
+ }
+
+ private static string GetPortableRuntimeIdentifier()
+ {
+ string osPart = OperatingSystem.IsWindows() ? "win" : (OperatingSystem.IsMacOS() ? "osx" : "linux");
+ return $"{osPart}-{Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.RuntimeArchitecture}";
+ }
+
+ [IterationCleanup]
+ public void IterationCleanup()
+ {
+ try
+ {
+ // Ensure assemblies are unloaded
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ StopBuildServer();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Error during cleanup: {ex}");
+ }
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ Process.RunToCompletion(
+ DotnetMuxer.Path.FullName,
+ $"build-server shutdown",
+ workingDirectory: _coverletTestSubjectSourcePath);
+ }
+
+ [Benchmark(Description = "Simulate Workflow")]
+ public void SimulateWorkflow()
+ {
+ _logger.LogInformation($"SimulateWorkflow Directory: {Directory.GetCurrentDirectory()}");
+ _coverletTestSubjectArtifactPath = Directory.GetCurrentDirectory();
+ _coverletTestSubjectDllPath = Path.Combine(Directory.GetCurrentDirectory(), "coverlet.testsubject.dll");
+
+ string pdbPath = Path.ChangeExtension(_coverletTestSubjectDllPath, ".pdb");
+ if (!File.Exists(pdbPath))
+ {
+ throw new FileNotFoundException($"Test subject PDB not found at: {pdbPath}");
+ }
+
+ _coverageParameters = new CoverageParameters
+ {
+ Module = _coverletTestSubjectDllPath,
+ IncludeFilters = ["[coverlet.testsubject]*"],
+ IncludeDirectories = [_coverletTestSubjectArtifactPath],
+ ExcludeFilters = null,
+ ExcludedSourceFiles = null,
+ ExcludeAttributes = null,
+ IncludeTestAssembly = true,
+ SingleHit = false,
+ MergeWith = string.Empty,
+ UseSourceLink = false,
+ SkipAutoProps = true,
+ DeterministicReport = false,
+ ExcludeAssembliesWithoutSources = "None",
+ };
+
+ Phase1_InstrumentAssemblies();
+ Phase2_GenerateHits();
+ Phase3_ProcessResults();
+ }
+
+ ///
+ /// Instruments SUT assembly 'coverlet.testsubject' for code coverage analysis.
+ ///
+ /// This method processes the SUT assembly, instruments them for code coverage.
+ /// If no modules are instrumented, an is thrown.
+ /// Thrown if no modules are instrumented during the operation.
+
+ public void Phase1_InstrumentAssemblies()
+ {
+ _logger.LogInformation($"Instrumenting assembly at: {_coverletTestSubjectDllPath}");
+
+ // First verify all required assemblies are available
+ var requiredAssemblies = new[]
+ {
+ ("Mono.Cecil", typeof(Mono.Cecil.ModuleDefinition).Assembly.Location),
+ ("Target Assembly", _coverletTestSubjectDllPath)
+ };
+
+ foreach (var (name, path) in requiredAssemblies)
+ {
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException($"Required assembly {name} not found at {path}");
+ }
+ _logger.LogVerbose($"Found required assembly {name} at {path}");
+
+ try
+ {
+ // Only validate the assembly format
+ if (IsDotNetAssembly(path).Result)
+ {
+ _logger.LogVerbose($"Successfully validated {name} ({Path.GetFileName(path)})");
+ }
+ else
+ {
+ throw new InvalidOperationException($"File is not a valid .NET assembly: {path}");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Failed to validate {name}: {ex}");
+ throw;
+ }
+ }
+
+ // Initialize CoveragePrepareResult with parameters
+ _coveragePrepareResult = new CoveragePrepareResult
+ {
+ Identifier = Guid.NewGuid().ToString(),
+ ModuleOrDirectory = _coverletTestSubjectDllPath,
+ Results = Array.Empty(),
+ Parameters = _coverageParameters
+ };
+
+ // Verify PDB state before proceeding
+ if (!_instrumentationHelper.HasPdb(_coverletTestSubjectDllPath, out bool embedded))
+ {
+ throw new InvalidOperationException($"No PDB found for {_coverletTestSubjectDllPath}");
+ }
+ _logger.LogVerbose($"Found PDB (embedded: {embedded})");
+
+ // Log coverage parameters
+ _logger.LogVerbose("Coverage Parameters:");
+ _logger.LogVerbose($" Module: {_coveragePrepareResult.Parameters.Module}");
+ _logger.LogVerbose($" IncludeFilters: {string.Join(", ", _coveragePrepareResult.Parameters.IncludeFilters)}");
+ _logger.LogVerbose($" IncludeDirectories: {string.Join(", ", _coveragePrepareResult.Parameters.IncludeDirectories)}");
+
+ try
+ {
+ // Create coverage instance
+ _coverage = new Coverage(
+ _coverletTestSubjectDllPath,
+ _coverageParameters,
+ _logger,
+ _instrumentationHelper,
+ _fileSystem,
+ _sourceRootTranslator,
+ _cecilSymbolHelper
+ );
+
+ // Prepare modules for instrumentation
+ _coveragePrepareResult = _coverage.PrepareModules();
+
+ if (_coveragePrepareResult.Results.Length == 0)
+ {
+ throw (new InvalidOperationException("Instrumentation failed: _coveragePrepareResult.Results missing"));
+ }
+
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Instrumentation failed: {ex}");
+ if (ex.InnerException != null)
+ {
+ _logger.LogError($"Inner exception: {ex.InnerException}");
+ }
+ throw;
+ }
+ }
+
+ ///
+ /// Run SUT assembly 'coverlet.testsubject' to generate coverage hits.
+ ///
+ ///
+ ///
+ public void Phase2_GenerateHits()
+ {
+ _logger.LogInformation($"Execute BigClass: {_coverletTestSubjectDllPath}");
+ if (!File.Exists(_coverletTestSubjectDllPath))
+ {
+ throw new FileNotFoundException($"Instrumented assembly not found at: {_coverletTestSubjectDllPath}");
+ }
+
+ Process.RunToCompletion(
+ DotnetMuxer.Path.FullName,
+ $" {_coverletTestSubjectDllPath}",
+ workingDirectory: _coverletTestSubjectArtifactPath);
+
+ }
+
+ ///
+ /// Collects and processes the coverage results from SUT assembly 'coverlet.testsubject'.
+ ///
+ ///
+ public void Phase3_ProcessResults()
+ {
+ _logger.LogInformation("\nCalculating code coverage results: {_coverletTestSubjectDllPath}");
+ if (_coveragePrepareResult?.Results == null)
+ {
+ throw new InvalidOperationException("No coverage results available to process");
+ }
+
+ // Get coverage result
+ CoverageResult result = _coverage.GetCoverageResult();
+
+ IReporter reporter = new ReporterFactory("teamcity").CreateReporter();
+ if (reporter == null)
+ {
+ throw new InvalidOperationException($"Creating code coverage report failed");
+ }
+
+ if (reporter.OutputType == ReporterOutputType.Console)
+ {
+ _logger.LogInformation(" Outputting results to console", important: true);
+ _logger.LogInformation(reporter.Report(result, _sourceRootTranslator), important: true);
+ }
+
+ var coverageTable = new ConsoleTable("Module", "Line", "Branch", "Method");
+
+ CoverageDetails linePercentCalculation = CoverageSummary.CalculateLineCoverage(result.Modules);
+ CoverageDetails branchPercentCalculation = CoverageSummary.CalculateBranchCoverage(result.Modules);
+ CoverageDetails methodPercentCalculation = CoverageSummary.CalculateMethodCoverage(result.Modules);
+
+ double totalLinePercent = linePercentCalculation.Percent;
+ double totalBranchPercent = branchPercentCalculation.Percent;
+ double totalMethodPercent = methodPercentCalculation.Percent;
+
+ double averageLinePercent = linePercentCalculation.AverageModulePercent;
+ double averageBranchPercent = branchPercentCalculation.AverageModulePercent;
+ double averageMethodPercent = methodPercentCalculation.AverageModulePercent;
+
+ foreach (KeyValuePair _module in result.Modules)
+ {
+ double linePercent = CoverageSummary.CalculateLineCoverage(_module.Value).Percent;
+ double branchPercent = CoverageSummary.CalculateBranchCoverage(_module.Value).Percent;
+ double methodPercent = CoverageSummary.CalculateMethodCoverage(_module.Value).Percent;
+
+ coverageTable.AddRow(Path.GetFileNameWithoutExtension(_module.Key), $"{InvariantFormat(linePercent)}%", $"{InvariantFormat(branchPercent)}%", $"{InvariantFormat(methodPercent)}%");
+ }
+
+ _logger.LogInformation(coverageTable.ToStringAlternative());
+
+ }
+
+ static string InvariantFormat(double value) => value.ToString(CultureInfo.InvariantCulture);
+
+ static async Task IsDotNetAssembly(string fileName)
+ {
+ await using var stream = File.OpenRead(fileName);
+ return IsDotNetAssembly(stream);
+ }
+
+ static bool IsDotNetAssembly(Stream stream)
+ {
+ try
+ {
+ using var peReader = new PEReader(stream);
+ if (!peReader.HasMetadata)
+ return false;
+
+ // If peReader.PEHeaders doesn't throw, it is a valid PEImage
+ _ = peReader.PEHeaders.CorHeader;
+
+ var reader = peReader.GetMetadataReader();
+ return reader.IsAssembly;
+ }
+ catch (BadImageFormatException)
+ {
+ return false;
+ }
+ }
+ }
+
+ public class ConsoleLogger : Coverlet.Core.Abstractions.ILogger
+ {
+ private static readonly object s_sync = new();
+
+ public void LogVerbose(string message)
+ {
+ lock (s_sync)
+ {
+ WriteColoredMessage("[Verbose] ", ConsoleColor.Gray, message);
+ }
+ }
+
+ public void LogInformation(string message, bool important = false)
+ {
+ lock (s_sync)
+ {
+ var color = important ? ConsoleColor.Green : ConsoleColor.Gray;
+ WriteColoredMessage("[Info] ", color, message);
+ }
+ }
+
+ public void LogWarning(string message)
+ {
+ lock (s_sync)
+ {
+ WriteColoredMessage("[Warning] ", ConsoleColor.Yellow, message);
+ }
+ }
+
+ public void LogError(string message)
+ {
+ lock (s_sync)
+ {
+ WriteColoredMessage("[Error] ", ConsoleColor.Red, message);
+ }
+ }
+
+ public void LogError(Exception exception)
+ {
+ lock (s_sync)
+ {
+ WriteColoredMessage("[Error] ", ConsoleColor.Red, exception.ToString());
+ }
+ }
+
+ private static void WriteColoredMessage(string prefix, ConsoleColor color, string message)
+ {
+ var originalColor = Console.ForegroundColor;
+ try
+ {
+ Console.ForegroundColor = color;
+ Console.Write(prefix);
+ Console.ForegroundColor = originalColor;
+ Console.WriteLine(message);
+ }
+ finally
+ {
+ Console.ForegroundColor = originalColor;
+ }
+ }
+ }
+ public class RemoteExecution : IDisposable
+ {
+ private const int FailWaitTimeoutMilliseconds = 60 * 1000;
+ private readonly string _exceptionFile;
+
+ public RemoteExecution(System.Diagnostics.Process process, string className, string methodName, string exceptionFile)
+ {
+ Process = process;
+ ClassName = className;
+ MethodName = methodName;
+ _exceptionFile = exceptionFile;
+ }
+
+ public System.Diagnostics.Process Process { get; private set; }
+ public string ClassName { get; }
+ public string MethodName { get; }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this); // before Dispose(true) in case the Dispose call throws
+ Dispose(disposing: true);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ //Assert.True(disposing, $"A test {ClassName}.{MethodName} forgot to Dispose() the result of RemoteInvoke()");
+
+ if (Process != null)
+ {
+ //Assert.True(Process.WaitForExit(FailWaitTimeoutMilliseconds),
+ //$"Timed out after {FailWaitTimeoutMilliseconds}ms waiting for remote process {Process.Id}");
+
+ // A bit unorthodox to do throwing operations in a Dispose, but by doing it here we avoid
+ // needing to do this in every derived test and keep each test much simpler.
+ try
+ {
+ if (File.Exists(_exceptionFile))
+ {
+ throw new RemoteExecutionException(File.ReadAllText(_exceptionFile));
+ }
+ }
+ finally
+ {
+ if (File.Exists(_exceptionFile))
+ {
+ File.Delete(_exceptionFile);
+ }
+
+ // Cleanup
+ try { Process.Kill(); }
+ catch { } // ignore all cleanup errors
+ }
+
+ Process.Dispose();
+ Process = null;
+ }
+ }
+
+ private sealed class RemoteExecutionException : Exception
+ {
+ private readonly string _stackTrace;
+
+ internal RemoteExecutionException(string stackTrace)
+ : base("Remote process failed with an unhandled exception.")
+ {
+ _stackTrace = stackTrace;
+ }
+
+ public override string StackTrace => _stackTrace ?? base.StackTrace;
+ }
+ }
+ internal static class DotnetMuxer
+ {
+ public static FileInfo Path { get; }
+
+ static DotnetMuxer()
+ {
+ var muxerFileName = ExecutableName("dotnet");
+ var fxDepsFile = GetDataFromAppDomain("FX_DEPS_FILE");
+
+ if (string.IsNullOrEmpty(fxDepsFile))
+ {
+ return;
+ }
+
+ var muxerDir = new FileInfo(fxDepsFile).Directory?.Parent?.Parent?.Parent;
+
+ if (muxerDir is null)
+ {
+ return;
+ }
+
+ var muxerCandidate = new FileInfo(System.IO.Path.Combine(muxerDir.FullName, muxerFileName));
+
+ if (muxerCandidate.Exists)
+ {
+ Path = muxerCandidate;
+ }
+ else
+ {
+ throw new InvalidOperationException("no muxer!");
+ }
+ }
+
+ public static string GetDataFromAppDomain(string propertyName)
+ {
+ return AppContext.GetData(propertyName) as string;
+ }
+
+ public static string ExecutableName(this string withoutExtension) =>
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? withoutExtension + ".exe"
+ : withoutExtension;
+ }
+ public static class Process
+ {
+ public static int RunToCompletion(
+ string command,
+ string args,
+ Action stdOut = null,
+ Action stdErr = null,
+ string workingDirectory = null,
+ params (string key, string value)[] environmentVariables)
+ {
+ args ??= "";
+
+ var process = new System.Diagnostics.Process
+ {
+ StartInfo =
+ {
+ Arguments = args,
+ FileName = command,
+ RedirectStandardError = true,
+ RedirectStandardOutput = true,
+ RedirectStandardInput = true,
+ UseShellExecute = false
+ }
+ };
+
+ if (!string.IsNullOrWhiteSpace(workingDirectory))
+ {
+ process.StartInfo.WorkingDirectory = workingDirectory;
+ }
+
+ if (environmentVariables.Length > 0)
+ {
+ for (var i = 0; i < environmentVariables.Length; i++)
+ {
+ var (key, value) = environmentVariables[i];
+ process.StartInfo.Environment.Add(key, value);
+ }
+ }
+
+ if (stdOut != null)
+ {
+ process.OutputDataReceived += (sender, eventArgs) =>
+ {
+ if (eventArgs.Data != null)
+ {
+ stdOut(eventArgs.Data);
+ }
+ };
+ }
+
+ if (stdErr != null)
+ {
+ process.ErrorDataReceived += (sender, eventArgs) =>
+ {
+ if (eventArgs.Data != null)
+ {
+ stdErr(eventArgs.Data);
+ }
+ };
+ }
+
+ process.Start();
+
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ process.WaitForExit();
+
+ return process.ExitCode;
+ }
+ }
+}
+
diff --git a/test/coverlet.core.benchmark.tests/coverlet.core.benchmark.tests.csproj b/test/coverlet.core.benchmark.tests/coverlet.core.benchmark.tests.csproj
new file mode 100644
index 000000000..060cd5788
--- /dev/null
+++ b/test/coverlet.core.benchmark.tests/coverlet.core.benchmark.tests.csproj
@@ -0,0 +1,32 @@
+
+
+ net8.0
+ Exe
+ AnyCPU
+ portable
+ true
+ true
+ true
+ Release
+ false
+ $(NoWarn);CS0162
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/coverlet.core.benchmark.tests/coverlet.core.benchmark.tests.snk b/test/coverlet.core.benchmark.tests/coverlet.core.benchmark.tests.snk
new file mode 100644
index 000000000..b65a0a48d
Binary files /dev/null and b/test/coverlet.core.benchmark.tests/coverlet.core.benchmark.tests.snk differ
diff --git a/test/coverlet.core.tests.samples.netstandard/coverlet.core.tests.samples.netstandard.csproj b/test/coverlet.core.tests.samples.netstandard/coverlet.core.tests.samples.netstandard.csproj
index 36bc43a36..a7ce141f5 100644
--- a/test/coverlet.core.tests.samples.netstandard/coverlet.core.tests.samples.netstandard.csproj
+++ b/test/coverlet.core.tests.samples.netstandard/coverlet.core.tests.samples.netstandard.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/test/coverlet.core.tests/TestAssets/coverlet.testsubject.dll b/test/coverlet.core.tests/TestAssets/coverlet.testsubject.dll
new file mode 100644
index 000000000..85b377f99
Binary files /dev/null and b/test/coverlet.core.tests/TestAssets/coverlet.testsubject.dll differ
diff --git a/test/coverlet.core.tests/TestAssets/coverlet.testsubject.pdb b/test/coverlet.core.tests/TestAssets/coverlet.testsubject.pdb
new file mode 100644
index 000000000..07e4aa44d
Binary files /dev/null and b/test/coverlet.core.tests/TestAssets/coverlet.testsubject.pdb differ
diff --git a/test/coverlet.integration.tests/coverlet.integration.tests.csproj b/test/coverlet.integration.tests/coverlet.integration.tests.csproj
index 4c8cc9c50..ba247fb94 100644
--- a/test/coverlet.integration.tests/coverlet.integration.tests.csproj
+++ b/test/coverlet.integration.tests/coverlet.integration.tests.csproj
@@ -8,7 +8,6 @@
enable
false
true
- Exe
diff --git a/test/coverlet.msbuild.tasks.tests/coverlet.msbuild.tasks.tests.csproj b/test/coverlet.msbuild.tasks.tests/coverlet.msbuild.tasks.tests.csproj
index f363d52ac..8cb949dee 100644
--- a/test/coverlet.msbuild.tasks.tests/coverlet.msbuild.tasks.tests.csproj
+++ b/test/coverlet.msbuild.tasks.tests/coverlet.msbuild.tasks.tests.csproj
@@ -20,7 +20,7 @@
-
+
diff --git a/test/coverlet.testsubject/Program.cs b/test/coverlet.testsubject/Program.cs
new file mode 100644
index 000000000..90d9734d9
--- /dev/null
+++ b/test/coverlet.testsubject/Program.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace coverlet.testsubject
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ Console.WriteLine("Execute BigClass");
+ var big = new BigClass();
+ for (var i = 0; i < 1000; i++)
+ big.Do(i);
+ }
+ }
+}
diff --git a/test/coverlet.testsubject/coverlet.testsubject.csproj b/test/coverlet.testsubject/coverlet.testsubject.csproj
index c9332c47f..a89cd70b6 100644
--- a/test/coverlet.testsubject/coverlet.testsubject.csproj
+++ b/test/coverlet.testsubject/coverlet.testsubject.csproj
@@ -2,8 +2,20 @@
net8.0
+ Exe
false
false
+ portable
+ true
+ true
+
+
+ Always
+
+
+ Always
+
+