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 + +