diff --git a/.editorconfig b/.editorconfig index 3cfc4f441..85fec93f6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,8 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true +file_header_template = Copyright (c) Toni Solarin-Sodara\nLicensed under the MIT license. See LICENSE file in the project root for full license information. + # XML project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] indent_size = 2 diff --git a/Documentation/Examples/.editorconfig b/Documentation/Examples/.editorconfig new file mode 100644 index 000000000..d66ee0772 --- /dev/null +++ b/Documentation/Examples/.editorconfig @@ -0,0 +1,8 @@ +# top-most EditorConfig file +# We don't want to import other EditorConfig files and we want +# to ensure no rules are enabled for these asset source files. +root = true + +[*.cs] +# Default severity for all analyzer diagnostics +dotnet_analyzer_diagnostic.severity = none diff --git a/Documentation/Examples/MSBuild/DeterministicBuild/ClassLibrary1/Class1.cs b/Documentation/Examples/MSBuild/DeterministicBuild/ClassLibrary1/Class1.cs index 289865e7b..dcb976188 100644 --- a/Documentation/Examples/MSBuild/DeterministicBuild/ClassLibrary1/Class1.cs +++ b/Documentation/Examples/MSBuild/DeterministicBuild/ClassLibrary1/Class1.cs @@ -1,8 +1,6 @@ -using System; - -namespace ClassLibrary1 +namespace ClassLibrary1 { - public class Class1 + public class Class1 { public int Method() { diff --git a/Documentation/Examples/MSBuild/DeterministicBuild/ClassLibrary1/ClassLibrary1.csproj b/Documentation/Examples/MSBuild/DeterministicBuild/ClassLibrary1/ClassLibrary1.csproj index 9f5c4f4ab..8271e5471 100644 --- a/Documentation/Examples/MSBuild/DeterministicBuild/ClassLibrary1/ClassLibrary1.csproj +++ b/Documentation/Examples/MSBuild/DeterministicBuild/ClassLibrary1/ClassLibrary1.csproj @@ -2,6 +2,7 @@ netstandard2.0 + false diff --git a/Documentation/Examples/MSBuild/DeterministicBuild/Directory.Build.props b/Documentation/Examples/MSBuild/DeterministicBuild/Directory.Build.props index 5c27e11f6..34a4ddf95 100644 --- a/Documentation/Examples/MSBuild/DeterministicBuild/Directory.Build.props +++ b/Documentation/Examples/MSBuild/DeterministicBuild/Directory.Build.props @@ -8,6 +8,9 @@ - + + all + runtime; build; native; contentfiles; analyzers + diff --git a/Documentation/Examples/MSBuild/DeterministicBuild/Directory.Build.targets b/Documentation/Examples/MSBuild/DeterministicBuild/Directory.Build.targets index 95978e1e1..faf2349ba 100644 --- a/Documentation/Examples/MSBuild/DeterministicBuild/Directory.Build.targets +++ b/Documentation/Examples/MSBuild/DeterministicBuild/Directory.Build.targets @@ -1,4 +1,3 @@ - - + diff --git a/Documentation/Examples/MSBuild/DeterministicBuild/XUnitTestProject1/XUnitTestProject1.csproj b/Documentation/Examples/MSBuild/DeterministicBuild/XUnitTestProject1/XUnitTestProject1.csproj index f59a5b534..a7bac6432 100644 --- a/Documentation/Examples/MSBuild/DeterministicBuild/XUnitTestProject1/XUnitTestProject1.csproj +++ b/Documentation/Examples/MSBuild/DeterministicBuild/XUnitTestProject1/XUnitTestProject1.csproj @@ -3,6 +3,7 @@ net6.0 false + false diff --git a/Documentation/Examples/MSBuild/MergeWith/ClassLibrary1/Class1.cs b/Documentation/Examples/MSBuild/MergeWith/ClassLibrary1/Class1.cs index 289865e7b..dcb976188 100644 --- a/Documentation/Examples/MSBuild/MergeWith/ClassLibrary1/Class1.cs +++ b/Documentation/Examples/MSBuild/MergeWith/ClassLibrary1/Class1.cs @@ -1,8 +1,6 @@ -using System; - -namespace ClassLibrary1 +namespace ClassLibrary1 { - public class Class1 + public class Class1 { public int Method() { diff --git a/Documentation/Examples/MSBuild/MergeWith/ClassLibrary2/Class2.cs b/Documentation/Examples/MSBuild/MergeWith/ClassLibrary2/Class2.cs index aa93f5552..a2b5a7acc 100644 --- a/Documentation/Examples/MSBuild/MergeWith/ClassLibrary2/Class2.cs +++ b/Documentation/Examples/MSBuild/MergeWith/ClassLibrary2/Class2.cs @@ -1,8 +1,6 @@ -using System; - -namespace ClassLibrary2 +namespace ClassLibrary2 { - public class Class2 + public class Class2 { public int Method() { diff --git a/Documentation/Examples/MSBuild/MergeWith/ClassLibrary3/Class3.cs b/Documentation/Examples/MSBuild/MergeWith/ClassLibrary3/Class3.cs index 872291eb2..cfecc19cf 100644 --- a/Documentation/Examples/MSBuild/MergeWith/ClassLibrary3/Class3.cs +++ b/Documentation/Examples/MSBuild/MergeWith/ClassLibrary3/Class3.cs @@ -1,8 +1,6 @@ -using System; - -namespace ClassLibrary3 +namespace ClassLibrary3 { - public class Class3 + public class Class3 { public int Method() { diff --git a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject1/UnitTest1.cs b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject1/UnitTest1.cs index 5eeb5df3b..47655c8e7 100644 --- a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject1/UnitTest1.cs +++ b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject1/UnitTest1.cs @@ -1,9 +1,8 @@ -using System; -using Xunit; +using Xunit; namespace XUnitTestProject1 { - public class UnitTest1 + public class UnitTest1 { [Fact] public void Test1() diff --git a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject1/XUnitTestProject1.csproj b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject1/XUnitTestProject1.csproj index e549327fa..7092bbf88 100644 --- a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject1/XUnitTestProject1.csproj +++ b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject1/XUnitTestProject1.csproj @@ -3,6 +3,7 @@ net6.0 false + false diff --git a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject2/UnitTest2.cs b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject2/UnitTest2.cs index c6ce4b13d..48a8448a2 100644 --- a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject2/UnitTest2.cs +++ b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject2/UnitTest2.cs @@ -1,9 +1,8 @@ -using System; -using Xunit; +using Xunit; namespace XUnitTestProject2 { - public class UnitTest2 + public class UnitTest2 { [Fact] public void Test2() diff --git a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject2/XUnitTestProject2.csproj b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject2/XUnitTestProject2.csproj index be361474f..3af80f3d9 100644 --- a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject2/XUnitTestProject2.csproj +++ b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject2/XUnitTestProject2.csproj @@ -3,6 +3,7 @@ net6.0 false + false diff --git a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject3/UnitTest3.cs b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject3/UnitTest3.cs index 482d9ed81..34cb51c16 100644 --- a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject3/UnitTest3.cs +++ b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject3/UnitTest3.cs @@ -1,9 +1,8 @@ -using System; -using Xunit; +using Xunit; namespace XUnitTestProject3 { - public class UnitTest3 + public class UnitTest3 { [Fact] public void Test3() diff --git a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject3/XUnitTestProject3.csproj b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject3/XUnitTestProject3.csproj index c622ecd0a..668ef16a0 100644 --- a/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject3/XUnitTestProject3.csproj +++ b/Documentation/Examples/MSBuild/MergeWith/XUnitTestProject3/XUnitTestProject3.csproj @@ -3,6 +3,7 @@ net6.0 false + false diff --git a/Documentation/Examples/VSTest/DeterministicBuild/ClassLibrary1/ClassLibrary1.csproj b/Documentation/Examples/VSTest/DeterministicBuild/ClassLibrary1/ClassLibrary1.csproj index 9f5c4f4ab..8271e5471 100644 --- a/Documentation/Examples/VSTest/DeterministicBuild/ClassLibrary1/ClassLibrary1.csproj +++ b/Documentation/Examples/VSTest/DeterministicBuild/ClassLibrary1/ClassLibrary1.csproj @@ -2,6 +2,7 @@ netstandard2.0 + false diff --git a/Documentation/Examples/VSTest/DeterministicBuild/XUnitTestProject1/XUnitTestProject1.csproj b/Documentation/Examples/VSTest/DeterministicBuild/XUnitTestProject1/XUnitTestProject1.csproj index 642b38552..7f5fd73b3 100644 --- a/Documentation/Examples/VSTest/DeterministicBuild/XUnitTestProject1/XUnitTestProject1.csproj +++ b/Documentation/Examples/VSTest/DeterministicBuild/XUnitTestProject1/XUnitTestProject1.csproj @@ -1,4 +1,4 @@ - + net6.0 diff --git a/Documentation/Examples/VSTest/HelloWorld/ClassLibrary1/Class1.cs b/Documentation/Examples/VSTest/HelloWorld/ClassLibrary1/Class1.cs index 289865e7b..dcb976188 100644 --- a/Documentation/Examples/VSTest/HelloWorld/ClassLibrary1/Class1.cs +++ b/Documentation/Examples/VSTest/HelloWorld/ClassLibrary1/Class1.cs @@ -1,8 +1,6 @@ -using System; - -namespace ClassLibrary1 +namespace ClassLibrary1 { - public class Class1 + public class Class1 { public int Method() { diff --git a/Documentation/Examples/VSTest/HelloWorld/ClassLibrary1/ClassLibrary1.csproj b/Documentation/Examples/VSTest/HelloWorld/ClassLibrary1/ClassLibrary1.csproj index 9f5c4f4ab..8271e5471 100644 --- a/Documentation/Examples/VSTest/HelloWorld/ClassLibrary1/ClassLibrary1.csproj +++ b/Documentation/Examples/VSTest/HelloWorld/ClassLibrary1/ClassLibrary1.csproj @@ -2,6 +2,7 @@ netstandard2.0 + false diff --git a/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/runsettings.xml b/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/.runsettings similarity index 100% rename from Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/runsettings.xml rename to Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/.runsettings diff --git a/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/UnitTest1.cs b/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/UnitTest1.cs index 5eeb5df3b..47655c8e7 100644 --- a/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/UnitTest1.cs +++ b/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/UnitTest1.cs @@ -1,9 +1,8 @@ -using System; -using Xunit; +using Xunit; namespace XUnitTestProject1 { - public class UnitTest1 + public class UnitTest1 { [Fact] public void Test1() diff --git a/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/XUnitTestProject1.csproj b/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/XUnitTestProject1.csproj index 87be69a78..2dee501a6 100644 --- a/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/XUnitTestProject1.csproj +++ b/Documentation/Examples/VSTest/HelloWorld/XUnitTestProject1/XUnitTestProject1.csproj @@ -4,6 +4,7 @@ net6.0 false false + $(MSBuildThisFileDirectory).runsettings diff --git a/src/coverlet.collector/DataCollection/AttachmentManager.cs b/src/coverlet.collector/DataCollection/AttachmentManager.cs index 8259f71f3..9a4a68786 100644 --- a/src/coverlet.collector/DataCollection/AttachmentManager.cs +++ b/src/coverlet.collector/DataCollection/AttachmentManager.cs @@ -11,166 +11,166 @@ namespace Coverlet.Collector.DataCollection { - /// - /// Manages coverage report attachments - /// - internal class AttachmentManager : IDisposable - { - private readonly DataCollectionSink _dataSink; - private readonly TestPlatformEqtTrace _eqtTrace; - private readonly TestPlatformLogger _logger; - private readonly DataCollectionContext _dataCollectionContext; - private readonly IFileHelper _fileHelper; - private readonly IDirectoryHelper _directoryHelper; - private readonly ICountDownEvent _countDownEvent; - private readonly string _reportDirectory; + /// + /// Manages coverage report attachments + /// + internal class AttachmentManager : IDisposable + { + private readonly DataCollectionSink _dataSink; + private readonly TestPlatformEqtTrace _eqtTrace; + private readonly TestPlatformLogger _logger; + private readonly DataCollectionContext _dataCollectionContext; + private readonly IFileHelper _fileHelper; + private readonly IDirectoryHelper _directoryHelper; + private readonly ICountDownEvent _countDownEvent; + private readonly string _reportDirectory; - public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext dataCollectionContext, TestPlatformLogger logger, TestPlatformEqtTrace eqtTrace, ICountDownEvent countDownEvent) - : this(dataSink, - dataCollectionContext, - logger, - eqtTrace, - Guid.NewGuid().ToString(), - new FileHelper(), - new DirectoryHelper(), - countDownEvent) - { - } + public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext dataCollectionContext, TestPlatformLogger logger, TestPlatformEqtTrace eqtTrace, ICountDownEvent countDownEvent) + : this(dataSink, + dataCollectionContext, + logger, + eqtTrace, + Guid.NewGuid().ToString(), + new FileHelper(), + new DirectoryHelper(), + countDownEvent) + { + } - public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext dataCollectionContext, TestPlatformLogger logger, TestPlatformEqtTrace eqtTrace, string reportDirectoryName, IFileHelper fileHelper, IDirectoryHelper directoryHelper, ICountDownEvent countDownEvent) - { - // Store input variabless - _dataSink = dataSink; - _dataCollectionContext = dataCollectionContext; - _logger = logger; - _eqtTrace = eqtTrace; - _fileHelper = fileHelper; - _directoryHelper = directoryHelper; - _countDownEvent = countDownEvent; + public AttachmentManager(DataCollectionSink dataSink, DataCollectionContext dataCollectionContext, TestPlatformLogger logger, TestPlatformEqtTrace eqtTrace, string reportDirectoryName, IFileHelper fileHelper, IDirectoryHelper directoryHelper, ICountDownEvent countDownEvent) + { + // Store input variables + _dataSink = dataSink; + _dataCollectionContext = dataCollectionContext; + _logger = logger; + _eqtTrace = eqtTrace; + _fileHelper = fileHelper; + _directoryHelper = directoryHelper; + _countDownEvent = countDownEvent; - // Report directory to store the coverage reports. - _reportDirectory = Path.Combine(Path.GetTempPath(), reportDirectoryName); + // Report directory to store the coverage reports. + _reportDirectory = Path.Combine(Path.GetTempPath(), reportDirectoryName); - // Register events - _dataSink.SendFileCompleted += OnSendFileCompleted; - } + // Register events + _dataSink.SendFileCompleted += OnSendFileCompleted; + } - /// - /// Sends coverage report to test platform - /// - /// Coverage report - /// Coverage report file name - public void SendCoverageReport(string coverageReport, string coverageReportFileName) - { - // Save coverage report to file - string coverageReportPath = SaveCoverageReport(coverageReport, coverageReportFileName); + /// + /// Sends coverage report to test platform + /// + /// Coverage report + /// Coverage report file name + public void SendCoverageReport(string coverageReport, string coverageReportFileName) + { + // Save coverage report to file + string coverageReportPath = SaveCoverageReport(coverageReport, coverageReportFileName); - // Send coverage attachment to test platform. - SendAttachment(coverageReportPath); - } + // Send coverage attachment to test platform. + SendAttachment(coverageReportPath); + } - /// - /// Disposes attachment manager - /// - public void Dispose() + /// + /// Disposes attachment manager + /// + public void Dispose() + { + // Unregister events + try + { + _countDownEvent.Wait(); + if (_dataSink != null) { - // Unregister events - try - { - _countDownEvent.Wait(); - if (_dataSink != null) - { - _dataSink.SendFileCompleted -= OnSendFileCompleted; - } - CleanupReportDirectory(); - } - catch (Exception ex) - { - _logger.LogWarning(ex.ToString()); - } + _dataSink.SendFileCompleted -= OnSendFileCompleted; } + CleanupReportDirectory(); + } + catch (Exception ex) + { + _logger.LogWarning(ex.ToString()); + } + } - /// - /// Saves coverage report to file system - /// - /// Coverage report - /// Coverage report file name - /// Coverage report file path - private string SaveCoverageReport(string report, string reportFileName) - { - try - { - _directoryHelper.CreateDirectory(_reportDirectory); - string filePath = Path.Combine(_reportDirectory, reportFileName); - _fileHelper.WriteAllText(filePath, report); - _eqtTrace.Info("{0}: Saved coverage report to path: '{1}'", CoverletConstants.DataCollectorName, filePath); + /// + /// Saves coverage report to file system + /// + /// Coverage report + /// Coverage report file name + /// Coverage report file path + private string SaveCoverageReport(string report, string reportFileName) + { + try + { + _directoryHelper.CreateDirectory(_reportDirectory); + string filePath = Path.Combine(_reportDirectory, reportFileName); + _fileHelper.WriteAllText(filePath, report); + _eqtTrace.Info("{0}: Saved coverage report to path: '{1}'", CoverletConstants.DataCollectorName, filePath); - return filePath; - } - catch (Exception ex) - { - string errorMessage = string.Format(Resources.FailedToSaveCoverageReport, CoverletConstants.DataCollectorName, reportFileName, _reportDirectory); - throw new CoverletDataCollectorException(errorMessage, ex); - } - } + return filePath; + } + catch (Exception ex) + { + string errorMessage = string.Format(Resources.FailedToSaveCoverageReport, CoverletConstants.DataCollectorName, reportFileName, _reportDirectory); + throw new CoverletDataCollectorException(errorMessage, ex); + } + } - /// - /// SendFileCompleted event handler - /// - /// Sender - /// Event args - public void OnSendFileCompleted(object sender, AsyncCompletedEventArgs e) - { - try - { - _eqtTrace.Verbose("{0}: SendFileCompleted received", CoverletConstants.DataCollectorName); - } - catch (Exception ex) - { - _logger.LogWarning(ex.ToString()); - } - finally - { - _countDownEvent.Signal(); - } - } + /// + /// SendFileCompleted event handler + /// + /// Sender + /// Event args + public void OnSendFileCompleted(object sender, AsyncCompletedEventArgs e) + { + try + { + _eqtTrace.Verbose("{0}: SendFileCompleted received", CoverletConstants.DataCollectorName); + } + catch (Exception ex) + { + _logger.LogWarning(ex.ToString()); + } + finally + { + _countDownEvent.Signal(); + } + } - /// - /// Sends attachment file to test platform - /// - /// Attachment file path - private void SendAttachment(string attachmentPath) - { - if (_fileHelper.Exists(attachmentPath)) - { - // Send coverage attachment to test platform. - _eqtTrace.Verbose("{0}: Sending attachment to test platform", CoverletConstants.DataCollectorName); - _dataSink.SendFileAsync(_dataCollectionContext, attachmentPath, false); - } - else - { - _eqtTrace.Warning("{0}: Attachment file does not exist", CoverletConstants.DataCollectorName); - } - } + /// + /// Sends attachment file to test platform + /// + /// Attachment file path + private void SendAttachment(string attachmentPath) + { + if (_fileHelper.Exists(attachmentPath)) + { + // Send coverage attachment to test platform. + _eqtTrace.Verbose("{0}: Sending attachment to test platform", CoverletConstants.DataCollectorName); + _dataSink.SendFileAsync(_dataCollectionContext, attachmentPath, false); + } + else + { + _eqtTrace.Warning("{0}: Attachment file does not exist", CoverletConstants.DataCollectorName); + } + } - /// - /// Cleans up coverage report directory - /// - private void CleanupReportDirectory() + /// + /// Cleans up coverage report directory + /// + private void CleanupReportDirectory() + { + try + { + if (_directoryHelper.Exists(_reportDirectory)) { - try - { - if (_directoryHelper.Exists(_reportDirectory)) - { - _directoryHelper.Delete(_reportDirectory, true); - _eqtTrace.Verbose("{0}: Deleted report directory: '{1}'", CoverletConstants.DataCollectorName, _reportDirectory); - } - } - catch (Exception ex) - { - string errorMessage = string.Format(Resources.FailedToCleanupReportDirectory, CoverletConstants.DataCollectorName, _reportDirectory); - throw new CoverletDataCollectorException(errorMessage, ex); - } + _directoryHelper.Delete(_reportDirectory, true); + _eqtTrace.Verbose("{0}: Deleted report directory: '{1}'", CoverletConstants.DataCollectorName, _reportDirectory); } + } + catch (Exception ex) + { + string errorMessage = string.Format(Resources.FailedToCleanupReportDirectory, CoverletConstants.DataCollectorName, _reportDirectory); + throw new CoverletDataCollectorException(errorMessage, ex); + } } + } } diff --git a/src/coverlet.collector/DataCollection/CoverageManager.cs b/src/coverlet.collector/DataCollection/CoverageManager.cs index 89ec41eb1..d6f6052c8 100644 --- a/src/coverlet.collector/DataCollection/CoverageManager.cs +++ b/src/coverlet.collector/DataCollection/CoverageManager.cs @@ -14,109 +14,109 @@ namespace Coverlet.Collector.DataCollection { - /// - /// Manages coverlet coverage - /// - internal class CoverageManager - { - private readonly Coverage _coverage; - private readonly ICoverageWrapper _coverageWrapper; - private readonly ISourceRootTranslator _sourceRootTranslator; - public IReporter[] Reporters { get; } + /// + /// Manages coverlet coverage + /// + internal class CoverageManager + { + private readonly Coverage _coverage; + private readonly ICoverageWrapper _coverageWrapper; + private readonly ISourceRootTranslator _sourceRootTranslator; + public IReporter[] Reporters { get; } - public CoverageManager(CoverletSettings settings, TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, ICoverageWrapper coverageWrapper, - IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator, ICecilSymbolHelper cecilSymbolHelper) - : this(settings, - settings.ReportFormats.Select(format => - { - var reporterFactory = new ReporterFactory(format); - if (!reporterFactory.IsValidFormat()) - { - eqtTrace.Warning($"Invalid report format '{format}'"); - return null; - } - else - { - return reporterFactory.CreateReporter(); - } - }).Where(r => r != null).ToArray(), - new CoverletLogger(eqtTrace, logger), - coverageWrapper, instrumentationHelper, fileSystem, sourceRootTranslator, cecilSymbolHelper) + public CoverageManager(CoverletSettings settings, TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, ICoverageWrapper coverageWrapper, + IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator, ICecilSymbolHelper cecilSymbolHelper) + : this(settings, + settings.ReportFormats.Select(format => { - } + var reporterFactory = new ReporterFactory(format); + if (!reporterFactory.IsValidFormat()) + { + eqtTrace.Warning($"Invalid report format '{format}'"); + return null; + } + else + { + return reporterFactory.CreateReporter(); + } + }).Where(r => r != null).ToArray(), + new CoverletLogger(eqtTrace, logger), + coverageWrapper, instrumentationHelper, fileSystem, sourceRootTranslator, cecilSymbolHelper) + { + } - public CoverageManager(CoverletSettings settings, IReporter[] reporters, ILogger logger, ICoverageWrapper coverageWrapper, - IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator, ICecilSymbolHelper cecilSymbolHelper) - { - // Store input vars - Reporters = reporters; - _coverageWrapper = coverageWrapper; - _sourceRootTranslator = sourceRootTranslator; - // Coverage object - _coverage = _coverageWrapper.CreateCoverage(settings, logger, instrumentationHelper, fileSystem, sourceRootTranslator, cecilSymbolHelper); - } + public CoverageManager(CoverletSettings settings, IReporter[] reporters, ILogger logger, ICoverageWrapper coverageWrapper, + IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator, ICecilSymbolHelper cecilSymbolHelper) + { + // Store input vars + Reporters = reporters; + _coverageWrapper = coverageWrapper; + _sourceRootTranslator = sourceRootTranslator; + // Coverage object + _coverage = _coverageWrapper.CreateCoverage(settings, logger, instrumentationHelper, fileSystem, sourceRootTranslator, cecilSymbolHelper); + } - /// - /// Instrument modules - /// - public void InstrumentModules() - { - try - { - // Instrument modules - _coverageWrapper.PrepareModules(_coverage); - } - catch (Exception ex) - { - string errorMessage = string.Format(Resources.InstrumentationException, CoverletConstants.DataCollectorName); - throw new CoverletDataCollectorException(errorMessage, ex); - } - } + /// + /// Instrument modules + /// + public void InstrumentModules() + { + try + { + // Instrument modules + _coverageWrapper.PrepareModules(_coverage); + } + catch (Exception ex) + { + string errorMessage = string.Format(Resources.InstrumentationException, CoverletConstants.DataCollectorName); + throw new CoverletDataCollectorException(errorMessage, ex); + } + } - /// - /// Gets coverlet coverage reports - /// - /// Coverage reports - public IEnumerable<(string report, string fileName)> GetCoverageReports() - { - // Get coverage result - CoverageResult coverageResult = GetCoverageResult(); - return GetCoverageReports(coverageResult); - } + /// + /// Gets coverlet coverage reports + /// + /// Coverage reports + public IEnumerable<(string report, string fileName)> GetCoverageReports() + { + // Get coverage result + CoverageResult coverageResult = GetCoverageResult(); + return GetCoverageReports(coverageResult); + } - /// - /// Gets coverlet coverage result - /// - /// Coverage result - private CoverageResult GetCoverageResult() - { - try - { - return _coverageWrapper.GetCoverageResult(_coverage); - } - catch (Exception ex) - { - string errorMessage = string.Format(Resources.CoverageResultException, CoverletConstants.DataCollectorName); - throw new CoverletDataCollectorException(errorMessage, ex); - } - } + /// + /// Gets coverlet coverage result + /// + /// Coverage result + private CoverageResult GetCoverageResult() + { + try + { + return _coverageWrapper.GetCoverageResult(_coverage); + } + catch (Exception ex) + { + string errorMessage = string.Format(Resources.CoverageResultException, CoverletConstants.DataCollectorName); + throw new CoverletDataCollectorException(errorMessage, ex); + } + } - /// - /// Gets coverage reports from coverage result - /// - /// Coverage result - /// Coverage reports - private IEnumerable<(string report, string fileName)> GetCoverageReports(CoverageResult coverageResult) - { - try - { - return Reporters.Select(reporter => (reporter.Report(coverageResult, _sourceRootTranslator), Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension))); - } - catch (Exception ex) - { - string errorMessage = string.Format(Resources.CoverageReportException, CoverletConstants.DataCollectorName); - throw new CoverletDataCollectorException(errorMessage, ex); - } - } + /// + /// Gets coverage reports from coverage result + /// + /// Coverage result + /// Coverage reports + private IEnumerable<(string report, string fileName)> GetCoverageReports(CoverageResult coverageResult) + { + try + { + return Reporters.Select(reporter => (reporter.Report(coverageResult, _sourceRootTranslator), Path.ChangeExtension(CoverletConstants.DefaultFileName, reporter.Extension))); + } + catch (Exception ex) + { + string errorMessage = string.Format(Resources.CoverageReportException, CoverletConstants.DataCollectorName); + throw new CoverletDataCollectorException(errorMessage, ex); + } } + } } diff --git a/src/coverlet.collector/DataCollection/CoverageWrapper.cs b/src/coverlet.collector/DataCollection/CoverageWrapper.cs index ffd7d8a41..4e3f5a729 100644 --- a/src/coverlet.collector/DataCollection/CoverageWrapper.cs +++ b/src/coverlet.collector/DataCollection/CoverageWrapper.cs @@ -7,68 +7,68 @@ namespace Coverlet.Collector.DataCollection { + /// + /// Implementation for wrapping over Coverage class in coverlet.core + /// + internal class CoverageWrapper : ICoverageWrapper + { /// - /// Implementation for wrapping over Coverage class in coverlet.core + /// Creates a coverage object from given coverlet settings /// - internal class CoverageWrapper : ICoverageWrapper + /// Coverlet settings + /// Coverlet logger + /// + /// + /// + /// + /// Coverage object + public Coverage CreateCoverage(CoverletSettings settings, ILogger coverletLogger, IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator, ICecilSymbolHelper cecilSymbolHelper) { - /// - /// Creates a coverage object from given coverlet settings - /// - /// Coverlet settings - /// Coverlet logger - /// - /// - /// - /// - /// Coverage object - public Coverage CreateCoverage(CoverletSettings settings, ILogger coverletLogger, IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator, ICecilSymbolHelper cecilSymbolHelper) - { - CoverageParameters parameters = new() - { - IncludeFilters = settings.IncludeFilters, - IncludeDirectories = settings.IncludeDirectories, - ExcludeFilters = settings.ExcludeFilters, - ExcludedSourceFiles = settings.ExcludeSourceFiles, - ExcludeAttributes = settings.ExcludeAttributes, - IncludeTestAssembly = settings.IncludeTestAssembly, - SingleHit = settings.SingleHit, - MergeWith = settings.MergeWith, - UseSourceLink = settings.UseSourceLink, - SkipAutoProps = settings.SkipAutoProps, - DoesNotReturnAttributes = settings.DoesNotReturnAttributes, - DeterministicReport = settings.DeterministicReport, - ExcludeAssembliesWithoutSources = settings.ExcludeAssembliesWithoutSources - }; + CoverageParameters parameters = new() + { + IncludeFilters = settings.IncludeFilters, + IncludeDirectories = settings.IncludeDirectories, + ExcludeFilters = settings.ExcludeFilters, + ExcludedSourceFiles = settings.ExcludeSourceFiles, + ExcludeAttributes = settings.ExcludeAttributes, + IncludeTestAssembly = settings.IncludeTestAssembly, + SingleHit = settings.SingleHit, + MergeWith = settings.MergeWith, + UseSourceLink = settings.UseSourceLink, + SkipAutoProps = settings.SkipAutoProps, + DoesNotReturnAttributes = settings.DoesNotReturnAttributes, + DeterministicReport = settings.DeterministicReport, + ExcludeAssembliesWithoutSources = settings.ExcludeAssembliesWithoutSources + }; - return new Coverage( - settings.TestModule, - parameters, - coverletLogger, - instrumentationHelper, - fileSystem, - sourceRootTranslator, - cecilSymbolHelper); - } + return new Coverage( + settings.TestModule, + parameters, + coverletLogger, + instrumentationHelper, + fileSystem, + sourceRootTranslator, + cecilSymbolHelper); + } - /// - /// Gets the coverage result from provided coverage object - /// - /// Coverage - /// The coverage result - public CoverageResult GetCoverageResult(Coverage coverage) - { - return coverage.GetCoverageResult(); - } + /// + /// Gets the coverage result from provided coverage object + /// + /// Coverage + /// The coverage result + public CoverageResult GetCoverageResult(Coverage coverage) + { + return coverage.GetCoverageResult(); + } - /// - /// Prepares modules for getting coverage. - /// Wrapper over coverage.PrepareModules - /// - /// - public void PrepareModules(Coverage coverage) - { - coverage.PrepareModules(); - } + /// + /// Prepares modules for getting coverage. + /// Wrapper over coverage.PrepareModules + /// + /// + public void PrepareModules(Coverage coverage) + { + coverage.PrepareModules(); } + } } diff --git a/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs b/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs index d40a02e86..c95ba217b 100644 --- a/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs +++ b/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs @@ -16,223 +16,223 @@ namespace Coverlet.Collector.DataCollection { + /// + /// Coverlet coverage out-proc data collector. + /// + [DataCollectorTypeUri(CoverletConstants.DefaultUri)] + [DataCollectorFriendlyName(CoverletConstants.FriendlyName)] + public class CoverletCoverageCollector : DataCollector + { + private readonly TestPlatformEqtTrace _eqtTrace; + private readonly ICoverageWrapper _coverageWrapper; + private readonly ICountDownEventFactory _countDownEventFactory; + private readonly Func _serviceCollectionFactory; + + private DataCollectionEvents _events; + private TestPlatformLogger _logger; + private XmlElement _configurationElement; + private DataCollectionSink _dataSink; + private DataCollectionContext _dataCollectionContext; + private CoverageManager _coverageManager; + private IServiceProvider _serviceProvider; + + public CoverletCoverageCollector() : this(new TestPlatformEqtTrace(), new CoverageWrapper(), new CollectorCountdownEventFactory(), GetDefaultServiceCollection) + { + } + + internal CoverletCoverageCollector(TestPlatformEqtTrace eqtTrace, ICoverageWrapper coverageWrapper, ICountDownEventFactory countDownEventFactory, Func serviceCollectionFactory) : base() + { + _eqtTrace = eqtTrace; + _coverageWrapper = coverageWrapper; + _countDownEventFactory = countDownEventFactory; + _serviceCollectionFactory = serviceCollectionFactory; + } + + private void AttachDebugger() + { + if (int.TryParse(Environment.GetEnvironmentVariable("COVERLET_DATACOLLECTOR_OUTOFPROC_DEBUG"), out int result) && result == 1) + { + Debugger.Launch(); + Debugger.Break(); + } + } + /// - /// Coverlet coverage out-proc data collector. + /// Initializes data collector /// - [DataCollectorTypeUri(CoverletConstants.DefaultUri)] - [DataCollectorFriendlyName(CoverletConstants.FriendlyName)] - public class CoverletCoverageCollector : DataCollector + /// Configuration element + /// Events to register on + /// Data sink to send attachments to test platform + /// Test platform logger + /// Environment context + public override void Initialize( + XmlElement configurationElement, + DataCollectionEvents events, + DataCollectionSink dataSink, + DataCollectionLogger logger, + DataCollectionEnvironmentContext environmentContext) { - private readonly TestPlatformEqtTrace _eqtTrace; - private readonly ICoverageWrapper _coverageWrapper; - private readonly ICountDownEventFactory _countDownEventFactory; - private readonly Func _serviceCollectionFactory; - - private DataCollectionEvents _events; - private TestPlatformLogger _logger; - private XmlElement _configurationElement; - private DataCollectionSink _dataSink; - private DataCollectionContext _dataCollectionContext; - private CoverageManager _coverageManager; - private IServiceProvider _serviceProvider; - - public CoverletCoverageCollector() : this(new TestPlatformEqtTrace(), new CoverageWrapper(), new CollectorCountdownEventFactory(), GetDefaultServiceCollection) - { - } - internal CoverletCoverageCollector(TestPlatformEqtTrace eqtTrace, ICoverageWrapper coverageWrapper, ICountDownEventFactory countDownEventFactory, Func serviceCollectionFactory) : base() - { - _eqtTrace = eqtTrace; - _coverageWrapper = coverageWrapper; - _countDownEventFactory = countDownEventFactory; - _serviceCollectionFactory = serviceCollectionFactory; - } + AttachDebugger(); - private void AttachDebugger() - { - if (int.TryParse(Environment.GetEnvironmentVariable("COVERLET_DATACOLLECTOR_OUTOFPROC_DEBUG"), out int result) && result == 1) - { - Debugger.Launch(); - Debugger.Break(); - } - } + if (_eqtTrace.IsInfoEnabled) + { + _eqtTrace.Info("Initializing {0} with configuration: '{1}'", CoverletConstants.DataCollectorName, configurationElement?.OuterXml); + } - /// - /// Initializes data collector - /// - /// Configuration element - /// Events to register on - /// Data sink to send attachments to test platform - /// Test platform logger - /// Environment context - public override void Initialize( - XmlElement configurationElement, - DataCollectionEvents events, - DataCollectionSink dataSink, - DataCollectionLogger logger, - DataCollectionEnvironmentContext environmentContext) - { + // Store input variables + _events = events; + _configurationElement = configurationElement; + _dataSink = dataSink; + _dataCollectionContext = environmentContext.SessionDataCollectionContext; + _logger = new TestPlatformLogger(logger, _dataCollectionContext); - AttachDebugger(); + // Register events + _events.SessionStart += OnSessionStart; + _events.SessionEnd += OnSessionEnd; + } - if (_eqtTrace.IsInfoEnabled) - { - _eqtTrace.Info("Initializing {0} with configuration: '{1}'", CoverletConstants.DataCollectorName, configurationElement?.OuterXml); - } + /// + /// Disposes the data collector + /// + /// Disposing flag + protected override void Dispose(bool disposing) + { + _eqtTrace.Verbose("{0}: Disposing", CoverletConstants.DataCollectorName); - // Store input variables - _events = events; - _configurationElement = configurationElement; - _dataSink = dataSink; - _dataCollectionContext = environmentContext.SessionDataCollectionContext; - _logger = new TestPlatformLogger(logger, _dataCollectionContext); + // Unregister events + if (_events != null) + { + _events.SessionStart -= OnSessionStart; + _events.SessionEnd -= OnSessionEnd; + } - // Register events - _events.SessionStart += OnSessionStart; - _events.SessionEnd += OnSessionEnd; - } + // Remove vars + _events = null; + _dataSink = null; + _coverageManager = null; - /// - /// Disposes the data collector - /// - /// Disposing flag - protected override void Dispose(bool disposing) - { - _eqtTrace.Verbose("{0}: Disposing", CoverletConstants.DataCollectorName); + base.Dispose(disposing); + } - // Unregister events - if (_events != null) - { - _events.SessionStart -= OnSessionStart; - _events.SessionEnd -= OnSessionEnd; - } + /// + /// SessionStart event handler + /// + /// Sender + /// Event args + private void OnSessionStart(object sender, SessionStartEventArgs sessionStartEventArgs) + { + _eqtTrace.Verbose("{0}: SessionStart received", CoverletConstants.DataCollectorName); + + try + { + // Get coverlet settings + IEnumerable testModules = GetTestModules(sessionStartEventArgs); + var coverletSettingsParser = new CoverletSettingsParser(_eqtTrace); + CoverletSettings coverletSettings = coverletSettingsParser.Parse(_configurationElement, testModules); + + // Build services container + _serviceProvider = _serviceCollectionFactory(_eqtTrace, _logger, coverletSettings.TestModule).BuildServiceProvider(); + + // Get coverage and attachment managers + _coverageManager = new CoverageManager(coverletSettings, _eqtTrace, _logger, _coverageWrapper, + _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), + _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService()); + + // Instrument modules + _coverageManager.InstrumentModules(); + } + catch (Exception ex) + { + _logger.LogWarning(ex.ToString()); + Dispose(true); + } + } - // Remove vars - _events = null; - _dataSink = null; - _coverageManager = null; + /// + /// SessionEnd event handler + /// + /// Sender + /// Event args + private void OnSessionEnd(object sender, SessionEndEventArgs e) + { + try + { + _eqtTrace.Verbose("{0}: SessionEnd received", CoverletConstants.DataCollectorName); - base.Dispose(disposing); - } + // Get coverage reports + IEnumerable<(string report, string fileName)> coverageReports = _coverageManager?.GetCoverageReports(); - /// - /// SessionStart event handler - /// - /// Sender - /// Event args - private void OnSessionStart(object sender, SessionStartEventArgs sessionStartEventArgs) + if (coverageReports != null && coverageReports.Any()) { - _eqtTrace.Verbose("{0}: SessionStart received", CoverletConstants.DataCollectorName); - - try - { - // Get coverlet settings - IEnumerable testModules = GetTestModules(sessionStartEventArgs); - var coverletSettingsParser = new CoverletSettingsParser(_eqtTrace); - CoverletSettings coverletSettings = coverletSettingsParser.Parse(_configurationElement, testModules); - - // Build services container - _serviceProvider = _serviceCollectionFactory(_eqtTrace, _logger, coverletSettings.TestModule).BuildServiceProvider(); - - // Get coverage and attachment managers - _coverageManager = new CoverageManager(coverletSettings, _eqtTrace, _logger, _coverageWrapper, - _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), - _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService()); - - // Instrument modules - _coverageManager.InstrumentModules(); - } - catch (Exception ex) - { - _logger.LogWarning(ex.ToString()); - Dispose(true); - } + // Send result attachments to test platform. + using var attachmentManager = new AttachmentManager(_dataSink, _dataCollectionContext, _logger, _eqtTrace, _countDownEventFactory.Create(coverageReports.Count(), TimeSpan.FromSeconds(30))); + foreach ((string report, string fileName) in coverageReports) + { + attachmentManager.SendCoverageReport(report, fileName); + } } - - /// - /// SessionEnd event handler - /// - /// Sender - /// Event args - private void OnSessionEnd(object sender, SessionEndEventArgs e) + else { - try - { - _eqtTrace.Verbose("{0}: SessionEnd received", CoverletConstants.DataCollectorName); - - // Get coverage reports - IEnumerable<(string report, string fileName)> coverageReports = _coverageManager?.GetCoverageReports(); - - if (coverageReports != null && coverageReports.Any()) - { - // Send result attachments to test platform. - using var attachmentManager = new AttachmentManager(_dataSink, _dataCollectionContext, _logger, _eqtTrace, _countDownEventFactory.Create(coverageReports.Count(), TimeSpan.FromSeconds(30))); - foreach ((string report, string fileName) in coverageReports) - { - attachmentManager.SendCoverageReport(report, fileName); - } - } - else - { - _eqtTrace.Verbose("{0}: No coverage reports specified", CoverletConstants.DataCollectorName); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex.ToString()); - Dispose(true); - } + _eqtTrace.Verbose("{0}: No coverage reports specified", CoverletConstants.DataCollectorName); } + } + catch (Exception ex) + { + _logger.LogWarning(ex.ToString()); + Dispose(true); + } + } - /// - /// Gets test modules - /// - /// Event args - /// Test modules list - private IEnumerable GetTestModules(SessionStartEventArgs sessionStartEventArgs) + /// + /// Gets test modules + /// + /// Event args + /// Test modules list + private IEnumerable GetTestModules(SessionStartEventArgs sessionStartEventArgs) + { + try + { + IEnumerable testModules = GetPropertyValueWrapper(sessionStartEventArgs); + if (_eqtTrace.IsInfoEnabled) { - try - { - IEnumerable testModules = GetPropertyValueWrapper(sessionStartEventArgs); - if (_eqtTrace.IsInfoEnabled) - { - _eqtTrace.Info("{0}: TestModules: '{1}'", - CoverletConstants.DataCollectorName, - string.Join(",", testModules ?? Enumerable.Empty())); - } - return testModules; - } - catch (MissingMethodException ex) - { - throw new MissingMethodException("Make sure to use .NET core SDK Version >= 2.2.300", ex); - } + _eqtTrace.Info("{0}: TestModules: '{1}'", + CoverletConstants.DataCollectorName, + string.Join(",", testModules ?? Enumerable.Empty())); } + return testModules; + } + catch (MissingMethodException ex) + { + throw new MissingMethodException("Make sure to use .NET core SDK Version >= 2.2.300", ex); + } + } - /// - /// Wraps GetPropertyValue to catch possible MissingMethodException on unsupported runtime - /// - /// - /// - private static IEnumerable GetPropertyValueWrapper(SessionStartEventArgs sessionStartEventArgs) - { - return sessionStartEventArgs.GetPropertyValue>(CoverletConstants.TestSourcesPropertyName); - } + /// + /// Wraps GetPropertyValue to catch possible MissingMethodException on unsupported runtime + /// + /// + /// + private static IEnumerable GetPropertyValueWrapper(SessionStartEventArgs sessionStartEventArgs) + { + return sessionStartEventArgs.GetPropertyValue>(CoverletConstants.TestSourcesPropertyName); + } - private static IServiceCollection GetDefaultServiceCollection(TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) - { - IServiceCollection serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); - // We need to keep singleton/static semantics - serviceCollection.AddSingleton(); - // We cache resolutions - serviceCollection.AddSingleton(serviceProvider => - new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService())); - serviceCollection.AddSingleton(); - return serviceCollection; - } + private static IServiceCollection GetDefaultServiceCollection(TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) + { + IServiceCollection serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); + // We need to keep singleton/static semantics + serviceCollection.AddSingleton(); + // We cache resolutions + serviceCollection.AddSingleton(serviceProvider => + new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService())); + serviceCollection.AddSingleton(); + return serviceCollection; } + } } diff --git a/src/coverlet.collector/DataCollection/CoverletLogger.cs b/src/coverlet.collector/DataCollection/CoverletLogger.cs index 42eb6a1b2..543f5417d 100644 --- a/src/coverlet.collector/DataCollection/CoverletLogger.cs +++ b/src/coverlet.collector/DataCollection/CoverletLogger.cs @@ -7,64 +7,64 @@ namespace Coverlet.Collector.DataCollection { + /// + /// Coverlet logger + /// + internal class CoverletLogger : ILogger + { + private readonly TestPlatformEqtTrace _eqtTrace; + private readonly TestPlatformLogger _logger; + + public CoverletLogger(TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger) + { + _eqtTrace = eqtTrace; + _logger = logger; + } + /// - /// Coverlet logger + /// Logs error /// - internal class CoverletLogger : ILogger + /// Error message + public void LogError(string message) { - private readonly TestPlatformEqtTrace _eqtTrace; - private readonly TestPlatformLogger _logger; - - public CoverletLogger(TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger) - { - _eqtTrace = eqtTrace; - _logger = logger; - } - - /// - /// Logs error - /// - /// Error message - public void LogError(string message) - { - _logger.LogWarning(message); - } + _logger.LogWarning(message); + } - /// - /// Logs error - /// - /// Exception to log - public void LogError(Exception exception) - { - _logger.LogWarning(exception.ToString()); - } + /// + /// Logs error + /// + /// Exception to log + public void LogError(Exception exception) + { + _logger.LogWarning(exception.ToString()); + } - /// - /// Logs information - /// - /// Information message - /// importance - public void LogInformation(string message, bool important = false) - { - _eqtTrace.Info(message); - } + /// + /// Logs information + /// + /// Information message + /// importance + public void LogInformation(string message, bool important = false) + { + _eqtTrace.Info(message); + } - /// - /// Logs verbose - /// - /// Verbose message - public void LogVerbose(string message) - { - _eqtTrace.Verbose(message); - } + /// + /// Logs verbose + /// + /// Verbose message + public void LogVerbose(string message) + { + _eqtTrace.Verbose(message); + } - /// - /// Logs warning - /// - /// Warning message - public void LogWarning(string message) - { - _eqtTrace.Warning(message); - } + /// + /// Logs warning + /// + /// Warning message + public void LogWarning(string message) + { + _eqtTrace.Warning(message); } + } } diff --git a/src/coverlet.collector/DataCollection/CoverletSettings.cs b/src/coverlet.collector/DataCollection/CoverletSettings.cs index 2ebde799f..0c80687f9 100644 --- a/src/coverlet.collector/DataCollection/CoverletSettings.cs +++ b/src/coverlet.collector/DataCollection/CoverletSettings.cs @@ -6,106 +6,106 @@ namespace Coverlet.Collector.DataCollection { + /// + /// Coverlet settings + /// + internal class CoverletSettings + { /// - /// Coverlet settings + /// Test module /// - internal class CoverletSettings + public string TestModule { get; set; } + + /// + /// Report formats + /// + public string[] ReportFormats { get; set; } + + /// + /// Filters to include + /// + public string[] IncludeFilters { get; set; } + + /// + /// Directories to include + /// + public string[] IncludeDirectories { get; set; } + + /// + /// Filters to exclude + /// + public string[] ExcludeFilters { get; set; } + + /// + /// Source files to exclude + /// + public string[] ExcludeSourceFiles { get; set; } + + /// + /// Attributes to exclude + /// + public string[] ExcludeAttributes { get; set; } + + /// + /// Coverate report path to merge with + /// + public string MergeWith { get; set; } + + /// + /// Use source link flag + /// + public bool UseSourceLink { get; set; } + + /// + /// Single hit flag + /// + public bool SingleHit { get; set; } + + /// + /// Includes test assembly + /// + public bool IncludeTestAssembly { get; set; } + + /// + /// Neither track nor record auto-implemented properties. + /// + public bool SkipAutoProps { get; set; } + + /// + /// Attributes that mark methods that never return. + /// + public string[] DoesNotReturnAttributes { get; set; } + + /// + /// DeterministicReport flag + /// + public bool DeterministicReport { get; set; } + + /// + /// Switch for heuristic to automatically exclude assemblies without source. + /// + public string ExcludeAssembliesWithoutSources { get; set; } + + public override string ToString() { - /// - /// Test module - /// - public string TestModule { get; set; } - - /// - /// Report formats - /// - public string[] ReportFormats { get; set; } - - /// - /// Filters to include - /// - public string[] IncludeFilters { get; set; } - - /// - /// Directories to include - /// - public string[] IncludeDirectories { get; set; } - - /// - /// Filters to exclude - /// - public string[] ExcludeFilters { get; set; } - - /// - /// Source files to exclude - /// - public string[] ExcludeSourceFiles { get; set; } - - /// - /// Attributes to exclude - /// - public string[] ExcludeAttributes { get; set; } - - /// - /// Coverate report path to merge with - /// - public string MergeWith { get; set; } - - /// - /// Use source link flag - /// - public bool UseSourceLink { get; set; } - - /// - /// Single hit flag - /// - public bool SingleHit { get; set; } - - /// - /// Includes test assembly - /// - public bool IncludeTestAssembly { get; set; } - - /// - /// Neither track nor record auto-implemented properties. - /// - public bool SkipAutoProps { get; set; } - - /// - /// Attributes that mark methods that never return. - /// - public string[] DoesNotReturnAttributes { get; set; } - - /// - /// DeterministicReport flag - /// - public bool DeterministicReport { get; set; } - - /// - /// Switch for heuristic to automatically exclude assemblies without source. - /// - public string ExcludeAssembliesWithoutSources { get; set; } - - public override string ToString() - { - var builder = new StringBuilder(); - - builder.AppendFormat("TestModule: '{0}', ", TestModule); - builder.AppendFormat("IncludeFilters: '{0}', ", string.Join(",", IncludeFilters ?? Enumerable.Empty())); - builder.AppendFormat("IncludeDirectories: '{0}', ", string.Join(",", IncludeDirectories ?? Enumerable.Empty())); - builder.AppendFormat("ExcludeFilters: '{0}', ", string.Join(",", ExcludeFilters ?? Enumerable.Empty())); - builder.AppendFormat("ExcludeSourceFiles: '{0}', ", string.Join(",", ExcludeSourceFiles ?? Enumerable.Empty())); - builder.AppendFormat("ExcludeAttributes: '{0}', ", string.Join(",", ExcludeAttributes ?? Enumerable.Empty())); - builder.AppendFormat("MergeWith: '{0}', ", MergeWith); - builder.AppendFormat("UseSourceLink: '{0}'", UseSourceLink); - builder.AppendFormat("SingleHit: '{0}'", SingleHit); - builder.AppendFormat("IncludeTestAssembly: '{0}'", IncludeTestAssembly); - builder.AppendFormat("SkipAutoProps: '{0}'", SkipAutoProps); - builder.AppendFormat("DoesNotReturnAttributes: '{0}'", string.Join(",", DoesNotReturnAttributes ?? Enumerable.Empty())); - builder.AppendFormat("DeterministicReport: '{0}'", DeterministicReport); - builder.AppendFormat("ExcludeAssembliesWithoutSources: '{0}'", ExcludeAssembliesWithoutSources); - - return builder.ToString(); - } + var builder = new StringBuilder(); + + builder.AppendFormat("TestModule: '{0}', ", TestModule); + builder.AppendFormat("IncludeFilters: '{0}', ", string.Join(",", IncludeFilters ?? Enumerable.Empty())); + builder.AppendFormat("IncludeDirectories: '{0}', ", string.Join(",", IncludeDirectories ?? Enumerable.Empty())); + builder.AppendFormat("ExcludeFilters: '{0}', ", string.Join(",", ExcludeFilters ?? Enumerable.Empty())); + builder.AppendFormat("ExcludeSourceFiles: '{0}', ", string.Join(",", ExcludeSourceFiles ?? Enumerable.Empty())); + builder.AppendFormat("ExcludeAttributes: '{0}', ", string.Join(",", ExcludeAttributes ?? Enumerable.Empty())); + builder.AppendFormat("MergeWith: '{0}', ", MergeWith); + builder.AppendFormat("UseSourceLink: '{0}'", UseSourceLink); + builder.AppendFormat("SingleHit: '{0}'", SingleHit); + builder.AppendFormat("IncludeTestAssembly: '{0}'", IncludeTestAssembly); + builder.AppendFormat("SkipAutoProps: '{0}'", SkipAutoProps); + builder.AppendFormat("DoesNotReturnAttributes: '{0}'", string.Join(",", DoesNotReturnAttributes ?? Enumerable.Empty())); + builder.AppendFormat("DeterministicReport: '{0}'", DeterministicReport); + builder.AppendFormat("ExcludeAssembliesWithoutSources: '{0}'", ExcludeAssembliesWithoutSources); + + return builder.ToString(); } + } } diff --git a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs index 3776c98d4..29584281e 100644 --- a/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs +++ b/src/coverlet.collector/DataCollection/CoverletSettingsParser.cs @@ -10,262 +10,262 @@ namespace Coverlet.Collector.DataCollection { + /// + /// Coverlet settings parser + /// + internal class CoverletSettingsParser + { + private readonly TestPlatformEqtTrace _eqtTrace; + + public CoverletSettingsParser(TestPlatformEqtTrace eqtTrace) + { + _eqtTrace = eqtTrace; + } + /// - /// Coverlet settings parser + /// Parser coverlet settings /// - internal class CoverletSettingsParser + /// Configuration element + /// Test modules + /// Coverlet settings + public CoverletSettings Parse(XmlElement configurationElement, IEnumerable testModules) { - private readonly TestPlatformEqtTrace _eqtTrace; + var coverletSettings = new CoverletSettings + { + TestModule = ParseTestModule(testModules) + }; - public CoverletSettingsParser(TestPlatformEqtTrace eqtTrace) - { - _eqtTrace = eqtTrace; - } + if (configurationElement != null) + { + coverletSettings.IncludeFilters = ParseIncludeFilters(configurationElement); + coverletSettings.IncludeDirectories = ParseIncludeDirectories(configurationElement); + coverletSettings.ExcludeAttributes = ParseExcludeAttributes(configurationElement); + coverletSettings.ExcludeSourceFiles = ParseExcludeSourceFiles(configurationElement); + coverletSettings.MergeWith = ParseMergeWith(configurationElement); + coverletSettings.UseSourceLink = ParseUseSourceLink(configurationElement); + coverletSettings.SingleHit = ParseSingleHit(configurationElement); + coverletSettings.IncludeTestAssembly = ParseIncludeTestAssembly(configurationElement); + coverletSettings.SkipAutoProps = ParseSkipAutoProps(configurationElement); + coverletSettings.DoesNotReturnAttributes = ParseDoesNotReturnAttributes(configurationElement); + coverletSettings.DeterministicReport = ParseDeterministicReport(configurationElement); + coverletSettings.ExcludeAssembliesWithoutSources = ParseExcludeAssembliesWithoutSources(configurationElement); + } - /// - /// Parser coverlet settings - /// - /// Configuration element - /// Test modules - /// Coverlet settings - public CoverletSettings Parse(XmlElement configurationElement, IEnumerable testModules) - { - var coverletSettings = new CoverletSettings - { - TestModule = ParseTestModule(testModules) - }; + coverletSettings.ReportFormats = ParseReportFormats(configurationElement); + coverletSettings.ExcludeFilters = ParseExcludeFilters(configurationElement); - if (configurationElement != null) - { - coverletSettings.IncludeFilters = ParseIncludeFilters(configurationElement); - coverletSettings.IncludeDirectories = ParseIncludeDirectories(configurationElement); - coverletSettings.ExcludeAttributes = ParseExcludeAttributes(configurationElement); - coverletSettings.ExcludeSourceFiles = ParseExcludeSourceFiles(configurationElement); - coverletSettings.MergeWith = ParseMergeWith(configurationElement); - coverletSettings.UseSourceLink = ParseUseSourceLink(configurationElement); - coverletSettings.SingleHit = ParseSingleHit(configurationElement); - coverletSettings.IncludeTestAssembly = ParseIncludeTestAssembly(configurationElement); - coverletSettings.SkipAutoProps = ParseSkipAutoProps(configurationElement); - coverletSettings.DoesNotReturnAttributes = ParseDoesNotReturnAttributes(configurationElement); - coverletSettings.DeterministicReport = ParseDeterministicReport(configurationElement); - coverletSettings.ExcludeAssembliesWithoutSources = ParseExcludeAssembliesWithoutSources(configurationElement); - } + if (_eqtTrace.IsVerboseEnabled) + { + _eqtTrace.Verbose("{0}: Initializing coverlet process with settings: \"{1}\"", CoverletConstants.DataCollectorName, coverletSettings.ToString()); + } - coverletSettings.ReportFormats = ParseReportFormats(configurationElement); - coverletSettings.ExcludeFilters = ParseExcludeFilters(configurationElement); + return coverletSettings; + } - if (_eqtTrace.IsVerboseEnabled) - { - _eqtTrace.Verbose("{0}: Initializing coverlet process with settings: \"{1}\"", CoverletConstants.DataCollectorName, coverletSettings.ToString()); - } + /// + /// Parses test module + /// + /// Test modules + /// Test module + private static string ParseTestModule(IEnumerable testModules) + { + // Validate if at least one source present. + if (testModules == null || !testModules.Any()) + { + string errorMessage = string.Format(Resources.NoTestModulesFound, CoverletConstants.DataCollectorName); + throw new CoverletDataCollectorException(errorMessage); + } - return coverletSettings; - } + // Note: + // 1) .NET core test run supports one testModule per run. Coverlet also supports one testModule per run. So, we are using first testSource only and ignoring others. + // 2) If and when .NET full is supported with coverlet OR .NET core starts supporting multiple testModules, revisit this code to use other testModules as well. + return testModules.FirstOrDefault(); + } - /// - /// Parses test module - /// - /// Test modules - /// Test module - private static string ParseTestModule(IEnumerable testModules) - { - // Validate if at least one source present. - if (testModules == null || !testModules.Any()) - { - string errorMessage = string.Format(Resources.NoTestModulesFound, CoverletConstants.DataCollectorName); - throw new CoverletDataCollectorException(errorMessage); - } + /// + /// Parse report formats + /// + /// Configuration element + /// Report formats + private static string[] ParseReportFormats(XmlElement configurationElement) + { + string[] formats = Array.Empty(); + if (configurationElement != null) + { + XmlElement reportFormatElement = configurationElement[CoverletConstants.ReportFormatElementName]; + formats = SplitElement(reportFormatElement); + } - // Note: - // 1) .NET core test run supports one testModule per run. Coverlet also supports one testModule per run. So, we are using first testSource only and ignoring others. - // 2) If and when .NET full is supported with coverlet OR .NET core starts supporting multiple testModules, revisit this code to use other testModules as well. - return testModules.FirstOrDefault(); - } + return formats is null || formats.Length == 0 ? new[] { CoverletConstants.DefaultReportFormat } : formats; + } - /// - /// Parse report formats - /// - /// Configuration element - /// Report formats - private static string[] ParseReportFormats(XmlElement configurationElement) - { - string[] formats = Array.Empty(); - if (configurationElement != null) - { - XmlElement reportFormatElement = configurationElement[CoverletConstants.ReportFormatElementName]; - formats = SplitElement(reportFormatElement); - } + /// + /// Parse filters to include + /// + /// Configuration element + /// Filters to include + private static string[] ParseIncludeFilters(XmlElement configurationElement) + { + XmlElement includeFiltersElement = configurationElement[CoverletConstants.IncludeFiltersElementName]; + return SplitElement(includeFiltersElement); + } - return formats is null || formats.Length == 0 ? new[] { CoverletConstants.DefaultReportFormat } : formats; - } + /// + /// Parse directories to include + /// + /// Configuration element + /// Directories to include + private static string[] ParseIncludeDirectories(XmlElement configurationElement) + { + XmlElement includeDirectoriesElement = configurationElement[CoverletConstants.IncludeDirectoriesElementName]; + return SplitElement(includeDirectoriesElement); + } - /// - /// Parse filters to include - /// - /// Configuration element - /// Filters to include - private static string[] ParseIncludeFilters(XmlElement configurationElement) - { - XmlElement includeFiltersElement = configurationElement[CoverletConstants.IncludeFiltersElementName]; - return SplitElement(includeFiltersElement); - } + /// + /// Parse filters to exclude + /// + /// Configuration element + /// Filters to exclude + private static string[] ParseExcludeFilters(XmlElement configurationElement) + { + var excludeFilters = new List { CoverletConstants.DefaultExcludeFilter }; - /// - /// Parse directories to include - /// - /// Configuration element - /// Directories to include - private static string[] ParseIncludeDirectories(XmlElement configurationElement) + if (configurationElement != null) + { + XmlElement excludeFiltersElement = configurationElement[CoverletConstants.ExcludeFiltersElementName]; + string[] filters = SplitElement(excludeFiltersElement); + if (filters != null) { - XmlElement includeDirectoriesElement = configurationElement[CoverletConstants.IncludeDirectoriesElementName]; - return SplitElement(includeDirectoriesElement); + excludeFilters.AddRange(filters); } + } - /// - /// Parse filters to exclude - /// - /// Configuration element - /// Filters to exclude - private static string[] ParseExcludeFilters(XmlElement configurationElement) - { - var excludeFilters = new List { CoverletConstants.DefaultExcludeFilter }; - - if (configurationElement != null) - { - XmlElement excludeFiltersElement = configurationElement[CoverletConstants.ExcludeFiltersElementName]; - string[] filters = SplitElement(excludeFiltersElement); - if (filters != null) - { - excludeFilters.AddRange(filters); - } - } - - return excludeFilters.ToArray(); - } + return excludeFilters.ToArray(); + } - /// - /// Parse source files to exclude - /// - /// Configuration element - /// Source files to exclude - private static string[] ParseExcludeSourceFiles(XmlElement configurationElement) - { - XmlElement excludeSourceFilesElement = configurationElement[CoverletConstants.ExcludeSourceFilesElementName]; - return SplitElement(excludeSourceFilesElement); - } + /// + /// Parse source files to exclude + /// + /// Configuration element + /// Source files to exclude + private static string[] ParseExcludeSourceFiles(XmlElement configurationElement) + { + XmlElement excludeSourceFilesElement = configurationElement[CoverletConstants.ExcludeSourceFilesElementName]; + return SplitElement(excludeSourceFilesElement); + } - /// - /// Parse attributes to exclude - /// - /// Configuration element - /// Attributes to exclude - private static string[] ParseExcludeAttributes(XmlElement configurationElement) - { - XmlElement excludeAttributesElement = configurationElement[CoverletConstants.ExcludeAttributesElementName]; - return SplitElement(excludeAttributesElement); - } + /// + /// Parse attributes to exclude + /// + /// Configuration element + /// Attributes to exclude + private static string[] ParseExcludeAttributes(XmlElement configurationElement) + { + XmlElement excludeAttributesElement = configurationElement[CoverletConstants.ExcludeAttributesElementName]; + return SplitElement(excludeAttributesElement); + } - /// - /// Parse merge with attribute - /// - /// Configuration element - /// Merge with attribute - private static string ParseMergeWith(XmlElement configurationElement) - { - XmlElement mergeWithElement = configurationElement[CoverletConstants.MergeWithElementName]; - return mergeWithElement?.InnerText; - } + /// + /// Parse merge with attribute + /// + /// Configuration element + /// Merge with attribute + private static string ParseMergeWith(XmlElement configurationElement) + { + XmlElement mergeWithElement = configurationElement[CoverletConstants.MergeWithElementName]; + return mergeWithElement?.InnerText; + } - /// - /// Parse use source link flag - /// - /// Configuration element - /// Use source link flag - private static bool ParseUseSourceLink(XmlElement configurationElement) - { - XmlElement useSourceLinkElement = configurationElement[CoverletConstants.UseSourceLinkElementName]; - bool.TryParse(useSourceLinkElement?.InnerText, out bool useSourceLink); - return useSourceLink; - } + /// + /// Parse use source link flag + /// + /// Configuration element + /// Use source link flag + private static bool ParseUseSourceLink(XmlElement configurationElement) + { + XmlElement useSourceLinkElement = configurationElement[CoverletConstants.UseSourceLinkElementName]; + bool.TryParse(useSourceLinkElement?.InnerText, out bool useSourceLink); + return useSourceLink; + } - /// - /// Parse single hit flag - /// - /// Configuration element - /// Single hit flag - private static bool ParseSingleHit(XmlElement configurationElement) - { - XmlElement singleHitElement = configurationElement[CoverletConstants.SingleHitElementName]; - bool.TryParse(singleHitElement?.InnerText, out bool singleHit); - return singleHit; - } + /// + /// Parse single hit flag + /// + /// Configuration element + /// Single hit flag + private static bool ParseSingleHit(XmlElement configurationElement) + { + XmlElement singleHitElement = configurationElement[CoverletConstants.SingleHitElementName]; + bool.TryParse(singleHitElement?.InnerText, out bool singleHit); + return singleHit; + } - /// - /// Parse ParseDeterministicReport flag - /// - /// Configuration element - /// ParseDeterministicReport flag - private static bool ParseDeterministicReport(XmlElement configurationElement) - { - XmlElement deterministicReportElement = configurationElement[CoverletConstants.DeterministicReport]; - bool.TryParse(deterministicReportElement?.InnerText, out bool deterministicReport); - return deterministicReport; - } + /// + /// Parse ParseDeterministicReport flag + /// + /// Configuration element + /// ParseDeterministicReport flag + private static bool ParseDeterministicReport(XmlElement configurationElement) + { + XmlElement deterministicReportElement = configurationElement[CoverletConstants.DeterministicReport]; + bool.TryParse(deterministicReportElement?.InnerText, out bool deterministicReport); + return deterministicReport; + } - /// - /// Parse ExcludeAssembliesWithoutSources flag - /// - /// Configuration element - /// ExcludeAssembliesWithoutSources flag - private static string ParseExcludeAssembliesWithoutSources(XmlElement configurationElement) - { - XmlElement instrumentModulesWithoutLocalSourcesElement = configurationElement[CoverletConstants.ExcludeAssembliesWithoutSources]; - return instrumentModulesWithoutLocalSourcesElement?.InnerText; - } + /// + /// Parse ExcludeAssembliesWithoutSources flag + /// + /// Configuration element + /// ExcludeAssembliesWithoutSources flag + private static string ParseExcludeAssembliesWithoutSources(XmlElement configurationElement) + { + XmlElement instrumentModulesWithoutLocalSourcesElement = configurationElement[CoverletConstants.ExcludeAssembliesWithoutSources]; + return instrumentModulesWithoutLocalSourcesElement?.InnerText; + } - /// - /// Parse include test assembly flag - /// - /// Configuration element - /// Include Test Assembly Flag - private static bool ParseIncludeTestAssembly(XmlElement configurationElement) - { - XmlElement includeTestAssemblyElement = configurationElement[CoverletConstants.IncludeTestAssemblyElementName]; - bool.TryParse(includeTestAssemblyElement?.InnerText, out bool includeTestAssembly); - return includeTestAssembly; - } + /// + /// Parse include test assembly flag + /// + /// Configuration element + /// Include Test Assembly Flag + private static bool ParseIncludeTestAssembly(XmlElement configurationElement) + { + XmlElement includeTestAssemblyElement = configurationElement[CoverletConstants.IncludeTestAssemblyElementName]; + bool.TryParse(includeTestAssemblyElement?.InnerText, out bool includeTestAssembly); + return includeTestAssembly; + } - /// - /// Parse skipautoprops flag - /// - /// Configuration element - /// Include Test Assembly Flag - private static bool ParseSkipAutoProps(XmlElement configurationElement) - { - XmlElement skipAutoPropsElement = configurationElement[CoverletConstants.SkipAutoProps]; - bool.TryParse(skipAutoPropsElement?.InnerText, out bool skipAutoProps); - return skipAutoProps; - } + /// + /// Parse skipautoprops flag + /// + /// Configuration element + /// Include Test Assembly Flag + private static bool ParseSkipAutoProps(XmlElement configurationElement) + { + XmlElement skipAutoPropsElement = configurationElement[CoverletConstants.SkipAutoProps]; + bool.TryParse(skipAutoPropsElement?.InnerText, out bool skipAutoProps); + return skipAutoProps; + } - /// - /// Parse attributes that mark methods that do not return. - /// - /// Configuration element - /// DoesNotReturn attributes - private static string[] ParseDoesNotReturnAttributes(XmlElement configurationElement) - { - XmlElement doesNotReturnAttributesElement = configurationElement[CoverletConstants.DoesNotReturnAttributesElementName]; - return SplitElement(doesNotReturnAttributesElement); - } + /// + /// Parse attributes that mark methods that do not return. + /// + /// Configuration element + /// DoesNotReturn attributes + private static string[] ParseDoesNotReturnAttributes(XmlElement configurationElement) + { + XmlElement doesNotReturnAttributesElement = configurationElement[CoverletConstants.DoesNotReturnAttributesElementName]; + return SplitElement(doesNotReturnAttributesElement); + } - /// - /// Splits a comma separated elements into an array - /// - /// The element to split - /// An array of the values in the element - private static string[] SplitElement(XmlElement element) - { - return element?.InnerText?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => value.Trim()).ToArray(); - } + /// + /// Splits a comma separated elements into an array + /// + /// The element to split + /// An array of the values in the element + private static string[] SplitElement(XmlElement element) + { + return element?.InnerText?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Where(value => !string.IsNullOrWhiteSpace(value)).Select(value => value.Trim()).ToArray(); } + } } diff --git a/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs b/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs index f682b2687..b61d75f55 100644 --- a/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs +++ b/src/coverlet.collector/InProcDataCollection/CoverletInProcDataCollector.cs @@ -14,117 +14,117 @@ namespace Coverlet.Collector.DataCollection { - public class CoverletInProcDataCollector : InProcDataCollection + public class CoverletInProcDataCollector : InProcDataCollection + { + private TestPlatformEqtTrace _eqtTrace; + private bool _enableExceptionLog; + + private void AttachDebugger() { - private TestPlatformEqtTrace _eqtTrace; - private bool _enableExceptionLog; + if (int.TryParse(Environment.GetEnvironmentVariable("COVERLET_DATACOLLECTOR_INPROC_DEBUG"), out int result) && result == 1) + { + Debugger.Launch(); + Debugger.Break(); + } + } - private void AttachDebugger() - { - if (int.TryParse(Environment.GetEnvironmentVariable("COVERLET_DATACOLLECTOR_INPROC_DEBUG"), out int result) && result == 1) - { - Debugger.Launch(); - Debugger.Break(); - } - } + private void EnableExceptionLog() + { + if (int.TryParse(Environment.GetEnvironmentVariable("COVERLET_DATACOLLECTOR_INPROC_EXCEPTIONLOG_ENABLED"), out int result) && result == 1) + { + _enableExceptionLog = true; + } + } - private void EnableExceptionLog() - { - if (int.TryParse(Environment.GetEnvironmentVariable("COVERLET_DATACOLLECTOR_INPROC_EXCEPTIONLOG_ENABLED"), out int result) && result == 1) - { - _enableExceptionLog = true; - } - } + public void Initialize(IDataCollectionSink dataCollectionSink) + { + AttachDebugger(); + EnableExceptionLog(); - public void Initialize(IDataCollectionSink dataCollectionSink) - { - AttachDebugger(); - EnableExceptionLog(); + _eqtTrace = new TestPlatformEqtTrace(); + _eqtTrace.Verbose("Initialize CoverletInProcDataCollector"); + } - _eqtTrace = new TestPlatformEqtTrace(); - _eqtTrace.Verbose("Initialize CoverletInProcDataCollector"); - } + public void TestCaseEnd(TestCaseEndArgs testCaseEndArgs) + { + } - public void TestCaseEnd(TestCaseEndArgs testCaseEndArgs) + public void TestCaseStart(TestCaseStartArgs testCaseStartArgs) + { + } + + public void TestSessionEnd(TestSessionEndArgs testSessionEndArgs) + { + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + Type injectedInstrumentationClass = GetInstrumentationClass(assembly); + if (injectedInstrumentationClass is null) { + continue; } - public void TestCaseStart(TestCaseStartArgs testCaseStartArgs) + try { + _eqtTrace.Verbose($"Calling ModuleTrackerTemplate.UnloadModule for '{injectedInstrumentationClass.Assembly.FullName}'"); + MethodInfo unloadModule = injectedInstrumentationClass.GetMethod(nameof(ModuleTrackerTemplate.UnloadModule), new[] { typeof(object), typeof(EventArgs) }); + unloadModule.Invoke(null, new[] { (object)this, EventArgs.Empty }); + injectedInstrumentationClass.GetField("FlushHitFile", BindingFlags.Static | BindingFlags.Public).SetValue(null, false); + _eqtTrace.Verbose($"Called ModuleTrackerTemplate.UnloadModule for '{injectedInstrumentationClass.Assembly.FullName}'"); } - - public void TestSessionEnd(TestSessionEndArgs testSessionEndArgs) + catch (Exception ex) { - foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - Type injectedInstrumentationClass = GetInstrumentationClass(assembly); - if (injectedInstrumentationClass is null) - { - continue; - } - - try - { - _eqtTrace.Verbose($"Calling ModuleTrackerTemplate.UnloadModule for '{injectedInstrumentationClass.Assembly.FullName}'"); - MethodInfo unloadModule = injectedInstrumentationClass.GetMethod(nameof(ModuleTrackerTemplate.UnloadModule), new[] { typeof(object), typeof(EventArgs) }); - unloadModule.Invoke(null, new[] { (object)this, EventArgs.Empty }); - injectedInstrumentationClass.GetField("FlushHitFile", BindingFlags.Static | BindingFlags.Public).SetValue(null, false); - _eqtTrace.Verbose($"Called ModuleTrackerTemplate.UnloadModule for '{injectedInstrumentationClass.Assembly.FullName}'"); - } - catch (Exception ex) - { - if (_enableExceptionLog) - { - _eqtTrace.Error("{0}: Failed to unload module with error: {1}", CoverletConstants.InProcDataCollectorName, ex); - string errorMessage = string.Format(Resources.FailedToUnloadModule, CoverletConstants.InProcDataCollectorName); - throw new CoverletDataCollectorException(errorMessage, ex); - } - } - } + if (_enableExceptionLog) + { + _eqtTrace.Error("{0}: Failed to unload module with error: {1}", CoverletConstants.InProcDataCollectorName, ex); + string errorMessage = string.Format(Resources.FailedToUnloadModule, CoverletConstants.InProcDataCollectorName); + throw new CoverletDataCollectorException(errorMessage, ex); + } } + } + } - public void TestSessionStart(TestSessionStartArgs testSessionStartArgs) + public void TestSessionStart(TestSessionStartArgs testSessionStartArgs) + { + } + + private Type GetInstrumentationClass(Assembly assembly) + { + try + { + foreach (Type type in assembly.GetTypes()) { + if (type.Namespace == "Coverlet.Core.Instrumentation.Tracker" + && type.Name.StartsWith(assembly.GetName().Name + "_")) + { + return type; + } } - private Type GetInstrumentationClass(Assembly assembly) + return null; + } + catch (Exception ex) + { + if (_enableExceptionLog) { - try + var exceptionString = new StringBuilder(); + exceptionString.AppendFormat("{0}: Failed to get Instrumentation class for assembly '{1}' with error: {2}", + CoverletConstants.InProcDataCollectorName, assembly, ex); + exceptionString.AppendLine(); + + if (ex is ReflectionTypeLoadException rtle) + { + exceptionString.AppendLine("ReflectionTypeLoadException list:"); + foreach (Exception loaderEx in rtle.LoaderExceptions) { - foreach (Type type in assembly.GetTypes()) - { - if (type.Namespace == "Coverlet.Core.Instrumentation.Tracker" - && type.Name.StartsWith(assembly.GetName().Name + "_")) - { - return type; - } - } - - return null; - } - catch (Exception ex) - { - if (_enableExceptionLog) - { - var exceptionString = new StringBuilder(); - exceptionString.AppendFormat("{0}: Failed to get Instrumentation class for assembly '{1}' with error: {2}", - CoverletConstants.InProcDataCollectorName, assembly, ex); - exceptionString.AppendLine(); - - if (ex is ReflectionTypeLoadException rtle) - { - exceptionString.AppendLine("ReflectionTypeLoadException list:"); - foreach (Exception loaderEx in rtle.LoaderExceptions) - { - exceptionString.AppendLine(loaderEx.ToString()); - } - } - - _eqtTrace.Warning(exceptionString.ToString()); - } - - return null; + exceptionString.AppendLine(loaderEx.ToString()); } + } + + _eqtTrace.Warning(exceptionString.ToString()); } + + return null; + } } + } } diff --git a/src/coverlet.collector/Properties/AssemblyInfo.cs b/src/coverlet.collector/Properties/AssemblyInfo.cs index 4d4a63712..9d5c4a633 100644 --- a/src/coverlet.collector/Properties/AssemblyInfo.cs +++ b/src/coverlet.collector/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Reflection; @@ -8,4 +8,4 @@ [assembly: InternalsVisibleTo("coverlet.core.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a82e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e86c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722aead64ad")] [assembly: InternalsVisibleTo("coverlet.collector.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100ed0ed6af9693182615b8dcadc83c918b8d36312f86cefc69539d67d4189cd1b89420e7c3871802ffef7f5ca7816c68ad856c77bf7c230cc07824d96aa5d1237eebd30e246b9a14e22695fb26b40c800f74ea96619092cbd3a5d430d6c003fc7a82e8ccd1e315b935105d9232fe9e99e8d7ff54bba6f191959338d4a3169df9b3")] // Needed to mock internal type https://github.com/Moq/moq4/wiki/Quickstart#advanced-features -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] \ No newline at end of file +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/coverlet.collector/Utilities/CountDownEvent.cs b/src/coverlet.collector/Utilities/CountDownEvent.cs index 265c54103..9a82a76b3 100644 --- a/src/coverlet.collector/Utilities/CountDownEvent.cs +++ b/src/coverlet.collector/Utilities/CountDownEvent.cs @@ -7,36 +7,36 @@ namespace Coverlet.Collector.Utilities { - internal class CollectorCountdownEventFactory : ICountDownEventFactory + internal class CollectorCountdownEventFactory : ICountDownEventFactory + { + public ICountDownEvent Create(int count, TimeSpan waitTimeout) { - public ICountDownEvent Create(int count, TimeSpan waitTimeout) - { - return new CollectorCountdownEvent(count, waitTimeout); - } + return new CollectorCountdownEvent(count, waitTimeout); } + } - internal class CollectorCountdownEvent : ICountDownEvent - { - private readonly CountdownEvent _countDownEvent; - private readonly TimeSpan _waitTimeout; + internal class CollectorCountdownEvent : ICountDownEvent + { + private readonly CountdownEvent _countDownEvent; + private readonly TimeSpan _waitTimeout; - public CollectorCountdownEvent(int count, TimeSpan waitTimeout) - { - _countDownEvent = new CountdownEvent(count); - _waitTimeout = waitTimeout; - } + public CollectorCountdownEvent(int count, TimeSpan waitTimeout) + { + _countDownEvent = new CountdownEvent(count); + _waitTimeout = waitTimeout; + } - public void Signal() - { - _countDownEvent.Signal(); - } + public void Signal() + { + _countDownEvent.Signal(); + } - public void Wait() - { - if (!_countDownEvent.Wait(_waitTimeout)) - { - throw new TimeoutException($"CollectorCountdownEvent timeout after {_waitTimeout}"); - } - } + public void Wait() + { + if (!_countDownEvent.Wait(_waitTimeout)) + { + throw new TimeoutException($"CollectorCountdownEvent timeout after {_waitTimeout}"); + } } + } } diff --git a/src/coverlet.collector/Utilities/CoverletConstants.cs b/src/coverlet.collector/Utilities/CoverletConstants.cs index 3a7bfa998..5ce4a79ef 100644 --- a/src/coverlet.collector/Utilities/CoverletConstants.cs +++ b/src/coverlet.collector/Utilities/CoverletConstants.cs @@ -3,29 +3,29 @@ namespace Coverlet.Collector.Utilities { - internal static class CoverletConstants - { - public const string FriendlyName = "XPlat code coverage"; - public const string DefaultUri = @"datacollector://Microsoft/CoverletCodeCoverage/1.0"; - public const string DataCollectorName = "CoverletCoverageDataCollector"; - public const string DefaultReportFormat = "cobertura"; - public const string DefaultFileName = "coverage"; - public const string IncludeFiltersElementName = "Include"; - public const string IncludeDirectoriesElementName = "IncludeDirectory"; - public const string ExcludeFiltersElementName = "Exclude"; - public const string ExcludeSourceFilesElementName = "ExcludeByFile"; - public const string ExcludeAttributesElementName = "ExcludeByAttribute"; - public const string MergeWithElementName = "MergeWith"; - public const string UseSourceLinkElementName = "UseSourceLink"; - public const string SingleHitElementName = "SingleHit"; - public const string IncludeTestAssemblyElementName = "IncludeTestAssembly"; - public const string TestSourcesPropertyName = "TestSources"; - public const string ReportFormatElementName = "Format"; - public const string DefaultExcludeFilter = "[coverlet.*]*"; - public const string InProcDataCollectorName = "CoverletInProcDataCollector"; - public const string SkipAutoProps = "SkipAutoProps"; - public const string DoesNotReturnAttributesElementName = "DoesNotReturnAttribute"; - public const string DeterministicReport = "DeterministicReport"; - public const string ExcludeAssembliesWithoutSources = "ExcludeAssembliesWithoutSources"; - } + internal static class CoverletConstants + { + public const string FriendlyName = "XPlat code coverage"; + public const string DefaultUri = @"datacollector://Microsoft/CoverletCodeCoverage/1.0"; + public const string DataCollectorName = "CoverletCoverageDataCollector"; + public const string DefaultReportFormat = "cobertura"; + public const string DefaultFileName = "coverage"; + public const string IncludeFiltersElementName = "Include"; + public const string IncludeDirectoriesElementName = "IncludeDirectory"; + public const string ExcludeFiltersElementName = "Exclude"; + public const string ExcludeSourceFilesElementName = "ExcludeByFile"; + public const string ExcludeAttributesElementName = "ExcludeByAttribute"; + public const string MergeWithElementName = "MergeWith"; + public const string UseSourceLinkElementName = "UseSourceLink"; + public const string SingleHitElementName = "SingleHit"; + public const string IncludeTestAssemblyElementName = "IncludeTestAssembly"; + public const string TestSourcesPropertyName = "TestSources"; + public const string ReportFormatElementName = "Format"; + public const string DefaultExcludeFilter = "[coverlet.*]*"; + public const string InProcDataCollectorName = "CoverletInProcDataCollector"; + public const string SkipAutoProps = "SkipAutoProps"; + public const string DoesNotReturnAttributesElementName = "DoesNotReturnAttribute"; + public const string DeterministicReport = "DeterministicReport"; + public const string ExcludeAssembliesWithoutSources = "ExcludeAssembliesWithoutSources"; + } } diff --git a/src/coverlet.collector/Utilities/CoverletDataCollectorException.cs b/src/coverlet.collector/Utilities/CoverletDataCollectorException.cs index 189b363f8..2b015d24e 100644 --- a/src/coverlet.collector/Utilities/CoverletDataCollectorException.cs +++ b/src/coverlet.collector/Utilities/CoverletDataCollectorException.cs @@ -5,14 +5,14 @@ namespace Coverlet.Collector.Utilities { - internal class CoverletDataCollectorException : Exception + internal class CoverletDataCollectorException : Exception + { + public CoverletDataCollectorException(string message) : base(message) { - public CoverletDataCollectorException(string message) : base(message) - { - } + } - public CoverletDataCollectorException(string message, Exception innerException) : base(message, innerException) - { - } + public CoverletDataCollectorException(string message, Exception innerException) : base(message, innerException) + { } + } } diff --git a/src/coverlet.collector/Utilities/DirectoryHelper.cs b/src/coverlet.collector/Utilities/DirectoryHelper.cs index c9800a21b..35ef0ccf5 100644 --- a/src/coverlet.collector/Utilities/DirectoryHelper.cs +++ b/src/coverlet.collector/Utilities/DirectoryHelper.cs @@ -6,25 +6,25 @@ namespace Coverlet.Collector.Utilities { + /// + internal class DirectoryHelper : IDirectoryHelper + { /// - internal class DirectoryHelper : IDirectoryHelper + public bool Exists(string path) { - /// - public bool Exists(string path) - { - return Directory.Exists(path); - } + return Directory.Exists(path); + } - /// - public void CreateDirectory(string path) - { - Directory.CreateDirectory(path); - } + /// + public void CreateDirectory(string path) + { + Directory.CreateDirectory(path); + } - /// - public void Delete(string path, bool recursive) - { - Directory.Delete(path, recursive); - } + /// + public void Delete(string path, bool recursive) + { + Directory.Delete(path, recursive); } + } } diff --git a/src/coverlet.collector/Utilities/FileHelper.cs b/src/coverlet.collector/Utilities/FileHelper.cs index f8a85b13e..7f6ecaaf4 100644 --- a/src/coverlet.collector/Utilities/FileHelper.cs +++ b/src/coverlet.collector/Utilities/FileHelper.cs @@ -6,19 +6,19 @@ namespace Coverlet.Collector.Utilities { + /// + internal class FileHelper : IFileHelper + { /// - internal class FileHelper : IFileHelper + public bool Exists(string path) { - /// - public bool Exists(string path) - { - return File.Exists(path); - } + return File.Exists(path); + } - /// - public void WriteAllText(string path, string contents) - { - File.WriteAllText(path, contents); - } + /// + public void WriteAllText(string path, string contents) + { + File.WriteAllText(path, contents); } + } } diff --git a/src/coverlet.collector/Utilities/Interfaces/ICountDown.cs b/src/coverlet.collector/Utilities/Interfaces/ICountDown.cs index 69eddbce4..a9b7b3354 100644 --- a/src/coverlet.collector/Utilities/Interfaces/ICountDown.cs +++ b/src/coverlet.collector/Utilities/Interfaces/ICountDown.cs @@ -6,33 +6,33 @@ namespace Coverlet.Collector.Utilities.Interfaces { + /// + /// Factory for ICountDownEvent + /// + internal interface ICountDownEventFactory + { /// - /// Factory for ICountDownEvent + /// Create ICountDownEvent instance /// - internal interface ICountDownEventFactory - { - /// - /// Create ICountDownEvent instance - /// - /// count of CountDownEvent - /// max wait - /// - ICountDownEvent Create(int count, TimeSpan waitTimeout); - } + /// count of CountDownEvent + /// max wait + /// + ICountDownEvent Create(int count, TimeSpan waitTimeout); + } + /// + /// Wrapper interface for CountDownEvent + /// + internal interface ICountDownEvent + { /// - /// Wrapper interface for CountDownEvent + /// Signal event /// - internal interface ICountDownEvent - { - /// - /// Signal event - /// - void Signal(); + void Signal(); - /// - /// Wait for event - /// - void Wait(); - } + /// + /// Wait for event + /// + void Wait(); + } } diff --git a/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs b/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs index 1a34612c0..48410be09 100644 --- a/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs +++ b/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs @@ -7,37 +7,37 @@ namespace Coverlet.Collector.Utilities.Interfaces { + /// + /// Wrapper interface for Coverage class in coverlet.core + /// Since the class is not testable, this interface is used to abstract methods for mocking in unit tests. + /// + internal interface ICoverageWrapper + { /// - /// Wrapper interface for Coverage class in coverlet.core - /// Since the class is not testable, this interface is used to abstract methods for mocking in unit tests. + /// Creates a coverage object from given coverlet settings /// - internal interface ICoverageWrapper - { - /// - /// Creates a coverage object from given coverlet settings - /// - /// Coverlet settings - /// Coverlet logger - /// Coverlet instrumentationHelper - /// Coverlet fileSystem - /// Coverlet sourceRootTranslator - /// - /// Coverage object - Coverage CreateCoverage(CoverletSettings settings, ILogger logger, IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator, ICecilSymbolHelper cecilSymbolHelper); + /// Coverlet settings + /// Coverlet logger + /// Coverlet instrumentationHelper + /// Coverlet fileSystem + /// Coverlet sourceRootTranslator + /// + /// Coverage object + Coverage CreateCoverage(CoverletSettings settings, ILogger logger, IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator, ICecilSymbolHelper cecilSymbolHelper); - /// - /// Gets the coverage result from provided coverage object - /// - /// Coverage - /// The coverage result - CoverageResult GetCoverageResult(Coverage coverage); + /// + /// Gets the coverage result from provided coverage object + /// + /// Coverage + /// The coverage result + CoverageResult GetCoverageResult(Coverage coverage); - /// - /// Prepares modules for getting coverage. - /// Wrapper over coverage.PrepareModules - /// - /// - void PrepareModules(Coverage coverage); + /// + /// Prepares modules for getting coverage. + /// Wrapper over coverage.PrepareModules + /// + /// + void PrepareModules(Coverage coverage); - } + } } diff --git a/src/coverlet.collector/Utilities/Interfaces/IDirectoryHelper.cs b/src/coverlet.collector/Utilities/Interfaces/IDirectoryHelper.cs index f148f21ea..07ee98c21 100644 --- a/src/coverlet.collector/Utilities/Interfaces/IDirectoryHelper.cs +++ b/src/coverlet.collector/Utilities/Interfaces/IDirectoryHelper.cs @@ -3,29 +3,29 @@ namespace Coverlet.Collector.Utilities.Interfaces { - interface IDirectoryHelper - { - /// - /// Determines whether the specified directory exists. - /// - /// The directory to check. - /// true if the caller has the required permissions and path contains the name of an existing directory; otherwise, false. - /// This method also returns false if path is null, an invalid path, or a zero-length string. - /// If the caller does not have sufficient permissions to read the specified file, - /// no exception is thrown and the method returns false regardless of the existence of path. - bool Exists(string path); + interface IDirectoryHelper + { + /// + /// Determines whether the specified directory exists. + /// + /// The directory to check. + /// true if the caller has the required permissions and path contains the name of an existing directory; otherwise, false. + /// This method also returns false if path is null, an invalid path, or a zero-length string. + /// If the caller does not have sufficient permissions to read the specified file, + /// no exception is thrown and the method returns false regardless of the existence of path. + bool Exists(string path); - /// - /// Creates all directories and subdirectories in the specified path unless they already exist. - /// - /// The directory to create. - void CreateDirectory(string directory); + /// + /// Creates all directories and subdirectories in the specified path unless they already exist. + /// + /// The directory to create. + void CreateDirectory(string directory); - /// - /// Deletes the specified directory and, if indicated, any subdirectories and files in the directory. - /// - /// The name of the directory to remove. - /// true to remove directories, subdirectories, and files in path; otherwise, false. - void Delete(string path, bool recursive); - } + /// + /// Deletes the specified directory and, if indicated, any subdirectories and files in the directory. + /// + /// The name of the directory to remove. + /// true to remove directories, subdirectories, and files in path; otherwise, false. + void Delete(string path, bool recursive); + } } diff --git a/src/coverlet.collector/Utilities/Interfaces/IFileHelper.cs b/src/coverlet.collector/Utilities/Interfaces/IFileHelper.cs index 1ddcc8678..c9c58bc21 100644 --- a/src/coverlet.collector/Utilities/Interfaces/IFileHelper.cs +++ b/src/coverlet.collector/Utilities/Interfaces/IFileHelper.cs @@ -3,24 +3,24 @@ namespace Coverlet.Collector.Utilities.Interfaces { - internal interface IFileHelper - { - /// - /// Determines whether the specified file exists. - /// - /// The file to check. - /// true if the caller has the required permissions and path contains the name of an existing file; otherwise, false. - /// This method also returns false if path is null, an invalid path, or a zero-length string. - /// If the caller does not have sufficient permissions to read the specified file, - /// no exception is thrown and the method returns false regardless of the existence of path. - bool Exists(string path); + internal interface IFileHelper + { + /// + /// Determines whether the specified file exists. + /// + /// The file to check. + /// true if the caller has the required permissions and path contains the name of an existing file; otherwise, false. + /// This method also returns false if path is null, an invalid path, or a zero-length string. + /// If the caller does not have sufficient permissions to read the specified file, + /// no exception is thrown and the method returns false regardless of the existence of path. + bool Exists(string path); - /// - /// Creates a new file, writes the specified string to the file, and then closes the file. - /// If the target file already exists, it is overwritten. - /// - /// The file to write to. - /// The string to write to the file. - void WriteAllText(string path, string contents); - } + /// + /// Creates a new file, writes the specified string to the file, and then closes the file. + /// If the target file already exists, it is overwritten. + /// + /// The file to write to. + /// The string to write to the file. + void WriteAllText(string path, string contents); + } } diff --git a/src/coverlet.collector/Utilities/TestPlatformEqtTrace.cs b/src/coverlet.collector/Utilities/TestPlatformEqtTrace.cs index 7c02f6d03..a5cbdf16a 100644 --- a/src/coverlet.collector/Utilities/TestPlatformEqtTrace.cs +++ b/src/coverlet.collector/Utilities/TestPlatformEqtTrace.cs @@ -5,52 +5,52 @@ namespace Coverlet.Collector.Utilities { + /// + /// Test platform eqttrace + /// + internal class TestPlatformEqtTrace + { + public bool IsInfoEnabled => EqtTrace.IsInfoEnabled; + public bool IsVerboseEnabled => EqtTrace.IsVerboseEnabled; + /// - /// Test platform eqttrace + /// Verbose logger /// - internal class TestPlatformEqtTrace + /// Format + /// Args + public void Verbose(string format, params object[] args) { - public bool IsInfoEnabled => EqtTrace.IsInfoEnabled; - public bool IsVerboseEnabled => EqtTrace.IsVerboseEnabled; - - /// - /// Verbose logger - /// - /// Format - /// Args - public void Verbose(string format, params object[] args) - { - EqtTrace.Verbose($"[coverlet]{format}", args); - } + EqtTrace.Verbose($"[coverlet]{format}", args); + } - /// - /// Warning logger - /// - /// Format - /// Args - public void Warning(string format, params object[] args) - { - EqtTrace.Warning($"[coverlet]{format}", args); - } + /// + /// Warning logger + /// + /// Format + /// Args + public void Warning(string format, params object[] args) + { + EqtTrace.Warning($"[coverlet]{format}", args); + } - /// - /// Info logger - /// - /// Format - /// Args - public void Info(string format, params object[] args) - { - EqtTrace.Info($"[coverlet]{format}", args); - } + /// + /// Info logger + /// + /// Format + /// Args + public void Info(string format, params object[] args) + { + EqtTrace.Info($"[coverlet]{format}", args); + } - /// - /// Error logger - /// - /// Format - /// Args - public void Error(string format, params object[] args) - { - EqtTrace.Error($"[coverlet]{format}", args); - } + /// + /// Error logger + /// + /// Format + /// Args + public void Error(string format, params object[] args) + { + EqtTrace.Error($"[coverlet]{format}", args); } + } } diff --git a/src/coverlet.collector/Utilities/TestPlatformLogger.cs b/src/coverlet.collector/Utilities/TestPlatformLogger.cs index 105e4c9d9..dd3560220 100644 --- a/src/coverlet.collector/Utilities/TestPlatformLogger.cs +++ b/src/coverlet.collector/Utilities/TestPlatformLogger.cs @@ -5,27 +5,27 @@ namespace Coverlet.Collector.Utilities { + /// + /// Test platform logger + /// + internal class TestPlatformLogger + { + private readonly DataCollectionLogger _logger; + private readonly DataCollectionContext _dataCollectionContext; + + public TestPlatformLogger(DataCollectionLogger logger, DataCollectionContext dataCollectionContext) + { + _logger = logger; + _dataCollectionContext = dataCollectionContext; + } + /// - /// Test platform logger + /// Log warning /// - internal class TestPlatformLogger + /// Warning message + public void LogWarning(string warning) { - private readonly DataCollectionLogger _logger; - private readonly DataCollectionContext _dataCollectionContext; - - public TestPlatformLogger(DataCollectionLogger logger, DataCollectionContext dataCollectionContext) - { - _logger = logger; - _dataCollectionContext = dataCollectionContext; - } - - /// - /// Log warning - /// - /// Warning message - public void LogWarning(string warning) - { - _logger.LogWarning(_dataCollectionContext, $"[coverlet]{warning}"); - } + _logger.LogWarning(_dataCollectionContext, $"[coverlet]{warning}"); } + } } diff --git a/src/coverlet.console/ExitCodes.cs b/src/coverlet.console/ExitCodes.cs index 670eda89a..93a7d395f 100644 --- a/src/coverlet.console/ExitCodes.cs +++ b/src/coverlet.console/ExitCodes.cs @@ -9,29 +9,29 @@ [Flags] internal enum CommandExitCodes { - /// - /// Indicates successful run of dotnet test without any test failure and coverage percentage above threshold if provided. - /// - Success = 0, + /// + /// Indicates successful run of dotnet test without any test failure and coverage percentage above threshold if provided. + /// + Success = 0, - /// - /// Indicates test failure by dotnet test. - /// - TestFailed = 1, + /// + /// Indicates test failure by dotnet test. + /// + TestFailed = 1, - /// - /// Indicates coverage percentage is below given threshold for one or more threshold type. - /// - CoverageBelowThreshold = 2, + /// + /// Indicates coverage percentage is below given threshold for one or more threshold type. + /// + CoverageBelowThreshold = 2, - /// - /// Indicates exception occurred during Coverlet process. - /// - Exception = 101, + /// + /// Indicates exception occurred during Coverlet process. + /// + Exception = 101, - /// - /// Indicates missing options or empty arguments for Coverlet process. - /// - CommandParsingException = 102 + /// + /// Indicates missing options or empty arguments for Coverlet process. + /// + CommandParsingException = 102 } diff --git a/src/coverlet.console/Logging/ConsoleLogger.cs b/src/coverlet.console/Logging/ConsoleLogger.cs index 1e41af504..30aca3a20 100644 --- a/src/coverlet.console/Logging/ConsoleLogger.cs +++ b/src/coverlet.console/Logging/ConsoleLogger.cs @@ -7,40 +7,40 @@ namespace Coverlet.Console.Logging { - class ConsoleLogger : ILogger - { - private static readonly object s_sync = new(); - - public LogLevel Level { get; set; } = LogLevel.Normal; + class ConsoleLogger : ILogger + { + private static readonly object s_sync = new(); + + public LogLevel Level { get; set; } = LogLevel.Normal; + + public void LogError(string message) => Log(LogLevel.Quiet, message, ConsoleColor.Red); - public void LogError(string message) => Log(LogLevel.Quiet, message, ConsoleColor.Red); + public void LogError(Exception exception) => LogError(exception.ToString()); - public void LogError(Exception exception) => LogError(exception.ToString()); + public void LogInformation(string message, bool important = false) => Log(important ? LogLevel.Minimal : LogLevel.Normal, message, ForegroundColor); - public void LogInformation(string message, bool important = false) => Log(important ? LogLevel.Minimal : LogLevel.Normal, message, ForegroundColor); + public void LogVerbose(string message) => Log(LogLevel.Detailed, message, ForegroundColor); - public void LogVerbose(string message) => Log(LogLevel.Detailed, message, ForegroundColor); + public void LogWarning(string message) => Log(LogLevel.Quiet, message, ConsoleColor.Yellow); - public void LogWarning(string message) => Log(LogLevel.Quiet, message, ConsoleColor.Yellow); + private void Log(LogLevel level, string message, ConsoleColor color) + { + if (level < Level) return; - private void Log(LogLevel level, string message, ConsoleColor color) + lock (s_sync) + { + ConsoleColor currentForegroundColor; + if (color != (currentForegroundColor = ForegroundColor)) + { + ForegroundColor = color; + WriteLine(message); + ForegroundColor = currentForegroundColor; + } + else { - if (level < Level) return; - - lock (s_sync) - { - ConsoleColor currentForegroundColor; - if (color != (currentForegroundColor = ForegroundColor)) - { - ForegroundColor = color; - WriteLine(message); - ForegroundColor = currentForegroundColor; - } - else - { - WriteLine(message); - } - } + WriteLine(message); } + } } + } } diff --git a/src/coverlet.console/Logging/LogLevel.cs b/src/coverlet.console/Logging/LogLevel.cs index 2e0cf7320..4035d07bd 100644 --- a/src/coverlet.console/Logging/LogLevel.cs +++ b/src/coverlet.console/Logging/LogLevel.cs @@ -1,33 +1,33 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Coverlet.Console.Logging { + /// + /// Defines logging severity levels. + /// + enum LogLevel + { /// - /// Defines logging severity levels. + /// Logs that track the general flow of the application. These logs should have long-term value. /// - enum LogLevel - { - /// - /// Logs that track the general flow of the application. These logs should have long-term value. - /// - Detailed = 0, + Detailed = 0, - /// - /// Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the - /// application execution to stop. - /// - Normal = 1, + /// + /// Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the + /// application execution to stop. + /// + Normal = 1, - /// - /// Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a - /// failure in the current activity, not an application-wide failure. - /// - Minimal = 2, + /// + /// Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a + /// failure in the current activity, not an application-wide failure. + /// + Minimal = 2, - /// - /// Not used for writing log messages. Specifies that a logging category should not write any messages except warning and errors. - /// - Quiet = 3 - } + /// + /// Not used for writing log messages. Specifies that a logging category should not write any messages except warning and errors. + /// + Quiet = 3 + } } diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index 73d848aaf..1a4bde021 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -3,14 +3,14 @@ using System; using System.Collections.Generic; +using System.CommandLine; using System.ComponentModel; using System.Diagnostics; using System.Globalization; -using System.Threading.Tasks; using System.IO; using System.Linq; using System.Text; -using System.CommandLine; +using System.Threading.Tasks; using ConsoleTables; using Coverlet.Console.Logging; using Coverlet.Core; @@ -23,379 +23,379 @@ namespace Coverlet.Console { - public static class Program + public static class Program + { + static int Main(string[] args) { - static int Main(string[] args) + var moduleOrAppDirectory = new Argument("path", "Path to the test assembly or application directory."); + var target = new Option(new[] { "--target", "-t" }, "Path to the test runner application.") { Arity = ArgumentArity.ZeroOrOne, IsRequired = true }; + var targs = new Option(new[] { "--targetargs", "-a" }, "Arguments to be passed to the test runner.") { Arity = ArgumentArity.ZeroOrOne }; + var output = new Option(new[] { "--output", "-o" }, "Output of the generated coverage report") { Arity = ArgumentArity.ZeroOrOne }; + var verbosity = new Option(new[] { "--verbosity", "-v" }, () => LogLevel.Normal, "Sets the verbosity level of the command. Allowed values are quiet, minimal, normal, detailed.") { Arity = ArgumentArity.ZeroOrOne }; + var formats = new Option(new[] { "--format", "-f" }, () => new[] { "json" }, "Format of the generated coverage report.") { Arity = ArgumentArity.ZeroOrMore }; + var threshold = new Option("--threshold", "Exits with error if the coverage % is below value.") { Arity = ArgumentArity.ZeroOrOne }; + var thresholdTypes = new Option>("--threshold-type", () => new List(new string[] { "line", "branch", "method" }), "Coverage type to apply the threshold to.").FromAmong("line", "branch", "method"); + var thresholdStat = new Option("--threshold-stat", () => ThresholdStatistic.Minimum, "Coverage statistic used to enforce the threshold value.") { Arity = ArgumentArity.ZeroOrMore }; + var excludeFilters = new Option("--exclude", "Filter expressions to exclude specific modules and types.") { Arity = ArgumentArity.ZeroOrMore }; + var includeFilters = new Option("--include", "Filter expressions to include only specific modules and types.") { Arity = ArgumentArity.ZeroOrMore }; + var excludedSourceFiles = new Option("--exclude-by-file", "Glob patterns specifying source files to exclude.") { Arity = ArgumentArity.ZeroOrMore }; + var includeDirectories = new Option("--include-directory", "Include directories containing additional assemblies to be instrumented.") { Arity = ArgumentArity.ZeroOrMore }; + var excludeAttributes = new Option("--exclude-by-attribute", "Attributes to exclude from code coverage.") { Arity = ArgumentArity.ZeroOrOne }; + var includeTestAssembly = new Option("--include-test-assembly", "Specifies whether to report code coverage of the test assembly.") { Arity = ArgumentArity.Zero }; + var singleHit = new Option("--single-hit", "Specifies whether to limit code coverage hit reporting to a single hit for each location") { Arity = ArgumentArity.Zero }; + var skipAutoProp = new Option("--skipautoprops", "Neither track nor record auto-implemented properties.") { Arity = ArgumentArity.Zero }; + var mergeWith = new Option("--merge-with", "Path to existing coverage result to merge.") { Arity = ArgumentArity.ZeroOrOne }; + var useSourceLink = new Option("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.") { Arity = ArgumentArity.Zero }; + var doesNotReturnAttributes = new Option("--does-not-return-attribute", "Attributes that mark methods that do not return") { Arity = ArgumentArity.ZeroOrMore }; + var excludeAssembliesWithoutSources = new Option("--exclude-assemblies-without-sources", "Specifies behaviour of heuristic to ignore assemblies with missing source documents.") { Arity = ArgumentArity.ZeroOrOne }; + + RootCommand rootCommand = new() + { + moduleOrAppDirectory, + target, + targs, + output, + verbosity, + formats, + threshold, + thresholdTypes, + thresholdStat, + excludeFilters, + includeFilters, + excludedSourceFiles, + includeDirectories, + excludeAttributes, + includeTestAssembly, + singleHit, + skipAutoProp, + mergeWith, + useSourceLink, + doesNotReturnAttributes, + excludeAssembliesWithoutSources + }; + + rootCommand.Description = "Cross platform .NET Core code coverage tool"; + + rootCommand.SetHandler(async (context) => + { + string moduleOrAppDirectoryValue = context.ParseResult.GetValueForArgument(moduleOrAppDirectory); + string targetValue = context.ParseResult.GetValueForOption(target); + string targsValue = context.ParseResult.GetValueForOption(targs); + string outputValue = context.ParseResult.GetValueForOption(output); + LogLevel verbosityValue = context.ParseResult.GetValueForOption(verbosity); + string[] formatsValue = context.ParseResult.GetValueForOption(formats); + string thresholdValue = context.ParseResult.GetValueForOption(threshold); + List thresholdTypesValue = context.ParseResult.GetValueForOption(thresholdTypes); + ThresholdStatistic thresholdStatValue = context.ParseResult.GetValueForOption(thresholdStat); + string[] excludeFiltersValue = context.ParseResult.GetValueForOption(excludeFilters); + string[] includeFiltersValue = context.ParseResult.GetValueForOption(includeFilters); + string[] excludedSourceFilesValue = context.ParseResult.GetValueForOption(excludedSourceFiles); + string[] includeDirectoriesValue = context.ParseResult.GetValueForOption(includeDirectories); + string[] excludeAttributesValue = context.ParseResult.GetValueForOption(excludeAttributes); + bool includeTestAssemblyValue = context.ParseResult.GetValueForOption(includeTestAssembly); + bool singleHitValue = context.ParseResult.GetValueForOption(singleHit); + bool skipAutoPropValue = context.ParseResult.GetValueForOption(skipAutoProp); + string mergeWithValue = context.ParseResult.GetValueForOption(mergeWith); + bool useSourceLinkValue = context.ParseResult.GetValueForOption(useSourceLink); + string[] doesNotReturnAttributesValue = context.ParseResult.GetValueForOption(doesNotReturnAttributes); + string excludeAssembliesWithoutSourcesValue = context.ParseResult.GetValueForOption(excludeAssembliesWithoutSources); + + if (string.IsNullOrEmpty(moduleOrAppDirectoryValue) || string.IsNullOrWhiteSpace(moduleOrAppDirectoryValue)) + throw new ArgumentException("No test assembly or application directory specified."); + + var taskStatus = await HandleCommand(moduleOrAppDirectoryValue, + targetValue, + targsValue, + outputValue, + verbosityValue, + formatsValue, + thresholdValue, + thresholdTypesValue, + thresholdStatValue, + excludeFiltersValue, + includeFiltersValue, + excludedSourceFilesValue, + includeDirectoriesValue, + excludeAttributesValue, + includeTestAssemblyValue, + singleHitValue, + skipAutoPropValue, + mergeWithValue, + useSourceLinkValue, + doesNotReturnAttributesValue, + excludeAssembliesWithoutSourcesValue); + context.ExitCode = taskStatus; + + }); + return rootCommand.Invoke(args); + } + private static Task HandleCommand(string moduleOrAppDirectory, + string target, + string targs, + string output, + LogLevel verbosity, + string[] formats, + string threshold, + List thresholdTypes, + ThresholdStatistic thresholdStat, + string[] excludeFilters, + string[] includeFilters, + string[] excludedSourceFiles, + string[] includeDirectories, + string[] excludeAttributes, + bool includeTestAssembly, + bool singleHit, + bool skipAutoProp, + string mergeWith, + bool useSourceLink, + string[] doesNotReturnAttributes, + string excludeAssembliesWithoutSources + ) + { + + IServiceCollection serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + // We need to keep singleton/static semantics + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(provider => new SourceRootTranslator(provider.GetRequiredService(), provider.GetRequiredService())); + serviceCollection.AddSingleton(); + + ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + + var logger = (ConsoleLogger)serviceProvider.GetService(); + IFileSystem fileSystem = serviceProvider.GetService(); + + // Adjust log level based on user input. + logger.Level = verbosity; + int exitCode = (int)CommandExitCodes.Success; + + try + { + CoverageParameters parameters = new() { - var moduleOrAppDirectory = new Argument("path", "Path to the test assembly or application directory."); - var target = new Option(new[] { "--target", "-t" }, "Path to the test runner application.") { Arity = ArgumentArity.ZeroOrOne, IsRequired = true }; - var targs = new Option(new[] { "--targetargs", "-a" }, "Arguments to be passed to the test runner.") { Arity = ArgumentArity.ZeroOrOne }; - var output = new Option(new[] { "--output", "-o" }, "Output of the generated coverage report") { Arity = ArgumentArity.ZeroOrOne }; - var verbosity = new Option(new[] { "--verbosity", "-v" }, () => LogLevel.Normal, "Sets the verbosity level of the command. Allowed values are quiet, minimal, normal, detailed.") { Arity = ArgumentArity.ZeroOrOne }; - var formats = new Option(new[] { "--format", "-f" }, () => new[] { "json" }, "Format of the generated coverage report.") { Arity = ArgumentArity.ZeroOrMore }; - var threshold = new Option("--threshold", "Exits with error if the coverage % is below value.") { Arity = ArgumentArity.ZeroOrOne }; - var thresholdTypes = new Option>("--threshold-type", () => new List(new string[] { "line", "branch", "method" }), "Coverage type to apply the threshold to.").FromAmong("line", "branch", "method"); - var thresholdStat = new Option("--threshold-stat", () => ThresholdStatistic.Minimum, "Coverage statistic used to enforce the threshold value.") { Arity = ArgumentArity.ZeroOrMore }; - var excludeFilters = new Option("--exclude", "Filter expressions to exclude specific modules and types.") { Arity = ArgumentArity.ZeroOrMore }; - var includeFilters = new Option("--include", "Filter expressions to include only specific modules and types.") { Arity = ArgumentArity.ZeroOrMore }; - var excludedSourceFiles = new Option("--exclude-by-file", "Glob patterns specifying source files to exclude.") { Arity = ArgumentArity.ZeroOrMore }; - var includeDirectories = new Option("--include-directory", "Include directories containing additional assemblies to be instrumented.") { Arity = ArgumentArity.ZeroOrMore }; - var excludeAttributes = new Option("--exclude-by-attribute", "Attributes to exclude from code coverage.") { Arity = ArgumentArity.ZeroOrOne }; - var includeTestAssembly = new Option("--include-test-assembly", "Specifies whether to report code coverage of the test assembly.") { Arity = ArgumentArity.Zero }; - var singleHit = new Option("--single-hit", "Specifies whether to limit code coverage hit reporting to a single hit for each location") { Arity = ArgumentArity.Zero }; - var skipAutoProp = new Option("--skipautoprops", "Neither track nor record auto-implemented properties.") { Arity = ArgumentArity.Zero }; - var mergeWith = new Option("--merge-with", "Path to existing coverage result to merge.") { Arity = ArgumentArity.ZeroOrOne }; - var useSourceLink = new Option("--use-source-link", "Specifies whether to use SourceLink URIs in place of file system paths.") { Arity = ArgumentArity.Zero }; - var doesNotReturnAttributes = new Option("--does-not-return-attribute", "Attributes that mark methods that do not return") { Arity = ArgumentArity.ZeroOrMore }; - var excludeAssembliesWithoutSources = new Option("--exclude-assemblies-without-sources", "Specifies behaviour of heuristic to ignore assemblies with missing source documents.") { Arity = ArgumentArity.ZeroOrOne }; - - RootCommand rootCommand = new () - { - moduleOrAppDirectory, - target, - targs, - output, - verbosity, - formats, - threshold, - thresholdTypes, - thresholdStat, - excludeFilters, - includeFilters, - excludedSourceFiles, - includeDirectories, - excludeAttributes, - includeTestAssembly, - singleHit, - skipAutoProp, - mergeWith, - useSourceLink, - doesNotReturnAttributes, - excludeAssembliesWithoutSources - }; - - rootCommand.Description = "Cross platform .NET Core code coverage tool"; - - rootCommand.SetHandler(async (context) => - { - string moduleOrAppDirectoryValue = context.ParseResult.GetValueForArgument(moduleOrAppDirectory); - string targetValue = context.ParseResult.GetValueForOption(target); - string targsValue = context.ParseResult.GetValueForOption(targs); - string outputValue = context.ParseResult.GetValueForOption(output); - LogLevel verbosityValue = context.ParseResult.GetValueForOption(verbosity); - string[] formatsValue = context.ParseResult.GetValueForOption(formats); - string thresholdValue = context.ParseResult.GetValueForOption(threshold); - List thresholdTypesValue = context.ParseResult.GetValueForOption(thresholdTypes); - ThresholdStatistic thresholdStatValue = context.ParseResult.GetValueForOption(thresholdStat); - string[] excludeFiltersValue = context.ParseResult.GetValueForOption(excludeFilters); - string[] includeFiltersValue = context.ParseResult.GetValueForOption(includeFilters); - string[] excludedSourceFilesValue = context.ParseResult.GetValueForOption(excludedSourceFiles); - string[] includeDirectoriesValue = context.ParseResult.GetValueForOption(includeDirectories); - string[] excludeAttributesValue = context.ParseResult.GetValueForOption(excludeAttributes); - bool includeTestAssemblyValue = context.ParseResult.GetValueForOption(includeTestAssembly); - bool singleHitValue = context.ParseResult.GetValueForOption(singleHit); - bool skipAutoPropValue = context.ParseResult.GetValueForOption(skipAutoProp); - string mergeWithValue = context.ParseResult.GetValueForOption(mergeWith); - bool useSourceLinkValue = context.ParseResult.GetValueForOption(useSourceLink); - string[] doesNotReturnAttributesValue = context.ParseResult.GetValueForOption(doesNotReturnAttributes); - string excludeAssembliesWithoutSourcesValue = context.ParseResult.GetValueForOption(excludeAssembliesWithoutSources); - - if (string.IsNullOrEmpty(moduleOrAppDirectoryValue) || string.IsNullOrWhiteSpace(moduleOrAppDirectoryValue)) - throw new ArgumentException("No test assembly or application directory specified."); - - var taskStatus = await HandleCommand(moduleOrAppDirectoryValue, - targetValue, - targsValue, - outputValue, - verbosityValue, - formatsValue, - thresholdValue, - thresholdTypesValue, - thresholdStatValue, - excludeFiltersValue, - includeFiltersValue, - excludedSourceFilesValue, - includeDirectoriesValue, - excludeAttributesValue, - includeTestAssemblyValue, - singleHitValue, - skipAutoPropValue, - mergeWithValue, - useSourceLinkValue, - doesNotReturnAttributesValue, - excludeAssembliesWithoutSourcesValue); - context.ExitCode = taskStatus; - - }); - return rootCommand.Invoke(args); - } - private static Task HandleCommand(string moduleOrAppDirectory, - string target, - string targs, - string output, - LogLevel verbosity, - string[] formats, - string threshold, - List thresholdTypes, - ThresholdStatistic thresholdStat, - string[] excludeFilters, - string[] includeFilters, - string[] excludedSourceFiles, - string[] includeDirectories, - string[] excludeAttributes, - bool includeTestAssembly, - bool singleHit, - bool skipAutoProp, - string mergeWith, - bool useSourceLink, - string[] doesNotReturnAttributes, - string excludeAssembliesWithoutSources - ) - { + IncludeFilters = includeFilters, + IncludeDirectories = includeDirectories, + ExcludeFilters = excludeFilters, + ExcludedSourceFiles = excludedSourceFiles, + ExcludeAttributes = excludeAttributes, + IncludeTestAssembly = includeTestAssembly, + SingleHit = singleHit, + MergeWith = mergeWith, + UseSourceLink = useSourceLink, + SkipAutoProps = skipAutoProp, + DoesNotReturnAttributes = doesNotReturnAttributes, + ExcludeAssembliesWithoutSources = excludeAssembliesWithoutSources + }; + ISourceRootTranslator sourceRootTranslator = serviceProvider.GetRequiredService(); + + Coverage coverage = new(moduleOrAppDirectory, + parameters, + logger, + serviceProvider.GetRequiredService(), + fileSystem, + sourceRootTranslator, + serviceProvider.GetRequiredService()); + coverage.PrepareModules(); + + Process process = new(); + process.StartInfo.FileName = target; + process.StartInfo.Arguments = targs ?? string.Empty; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.OutputDataReceived += (sender, eventArgs) => + { + if (!string.IsNullOrEmpty(eventArgs.Data)) + logger.LogInformation(eventArgs.Data, important: true); + }; - IServiceCollection serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - // We need to keep singleton/static semantics - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(provider => new SourceRootTranslator(provider.GetRequiredService(), provider.GetRequiredService())); - serviceCollection.AddSingleton(); + process.ErrorDataReceived += (sender, eventArgs) => + { + if (!string.IsNullOrEmpty(eventArgs.Data)) + logger.LogError(eventArgs.Data); + }; - ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); + process.Start(); - var logger = (ConsoleLogger)serviceProvider.GetService(); - IFileSystem fileSystem = serviceProvider.GetService(); + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); - // Adjust log level based on user input. - logger.Level = verbosity; - int exitCode = (int)CommandExitCodes.Success; + process.WaitForExit(); - try - { - CoverageParameters parameters = new() - { - IncludeFilters = includeFilters, - IncludeDirectories = includeDirectories, - ExcludeFilters = excludeFilters, - ExcludedSourceFiles = excludedSourceFiles, - ExcludeAttributes = excludeAttributes, - IncludeTestAssembly = includeTestAssembly, - SingleHit = singleHit, - MergeWith = mergeWith, - UseSourceLink = useSourceLink, - SkipAutoProps = skipAutoProp, - DoesNotReturnAttributes = doesNotReturnAttributes, - ExcludeAssembliesWithoutSources = excludeAssembliesWithoutSources - }; - ISourceRootTranslator sourceRootTranslator = serviceProvider.GetRequiredService(); - - Coverage coverage = new(moduleOrAppDirectory, - parameters, - logger, - serviceProvider.GetRequiredService(), - fileSystem, - sourceRootTranslator, - serviceProvider.GetRequiredService()); - coverage.PrepareModules(); - - Process process = new(); - process.StartInfo.FileName = target; - process.StartInfo.Arguments = targs ?? string.Empty; - process.StartInfo.CreateNoWindow = true; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.RedirectStandardError = true; - process.OutputDataReceived += (sender, eventArgs) => - { - if (!string.IsNullOrEmpty(eventArgs.Data)) - logger.LogInformation(eventArgs.Data, important: true); - }; - - process.ErrorDataReceived += (sender, eventArgs) => - { - if (!string.IsNullOrEmpty(eventArgs.Data)) - logger.LogError(eventArgs.Data); - }; - - process.Start(); - - process.BeginErrorReadLine(); - process.BeginOutputReadLine(); - - process.WaitForExit(); - - string dOutput = output != null ? output : Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar.ToString(); - - logger.LogInformation("\nCalculating coverage result..."); - - CoverageResult result = coverage.GetCoverageResult(); - - string directory = Path.GetDirectoryName(dOutput); - if (directory == string.Empty) - { - directory = Directory.GetCurrentDirectory(); - } - else if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - foreach (string format in formats) - { - IReporter reporter = new ReporterFactory(format).CreateReporter(); - if (reporter == null) - { - throw new Exception($"Specified output format '{format}' is not supported"); - } - - if (reporter.OutputType == ReporterOutputType.Console) - { - // Output to console - logger.LogInformation(" Outputting results to console", important: true); - logger.LogInformation(reporter.Report(result, sourceRootTranslator), important: true); - } - else - { - // Output to file - string filename = Path.GetFileName(dOutput); - filename = (filename == string.Empty) ? $"coverage.{reporter.Extension}" : filename; - filename = Path.HasExtension(filename) ? filename : $"{filename}.{reporter.Extension}"; - - string report = Path.Combine(directory, filename); - logger.LogInformation($" Generating report '{report}'", important: true); - fileSystem.WriteAllText(report, reporter.Report(result, sourceRootTranslator)); - } - } - - var thresholdTypeFlagQueue = new Queue(); - - foreach (string thresholdType in thresholdTypes) - { - if (thresholdType.Equals("line", StringComparison.OrdinalIgnoreCase)) - { - thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Line); - } - else if (thresholdType.Equals("branch", StringComparison.OrdinalIgnoreCase)) - { - thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Branch); - } - else if (thresholdType.Equals("method", StringComparison.OrdinalIgnoreCase)) - { - thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Method); - } - } - - var thresholdTypeFlagValues = new Dictionary(); - if (!string.IsNullOrEmpty(threshold) && threshold.Contains(',')) - { - IEnumerable thresholdValues = threshold.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()); - if (thresholdValues.Count() != thresholdTypeFlagQueue.Count) - { - throw new Exception($"Threshold type flag count ({thresholdTypeFlagQueue.Count}) and values count ({thresholdValues.Count()}) doesn't match"); - } - - foreach (string thresholdValue in thresholdValues) - { - if (double.TryParse(thresholdValue, out double value)) - { - thresholdTypeFlagValues[thresholdTypeFlagQueue.Dequeue()] = value; - } - else - { - throw new Exception($"Invalid threshold value must be numeric"); - } - } - } - else - { - double thresholdValue = threshold != null ? double.Parse(threshold) : 0; - - while (thresholdTypeFlagQueue.Any()) - { - thresholdTypeFlagValues[thresholdTypeFlagQueue.Dequeue()] = thresholdValue; - } - } - - var coverageTable = new ConsoleTable("Module", "Line", "Branch", "Method"); - var summary = new CoverageSummary(); - - CoverageDetails linePercentCalculation = summary.CalculateLineCoverage(result.Modules); - CoverageDetails branchPercentCalculation = summary.CalculateBranchCoverage(result.Modules); - CoverageDetails methodPercentCalculation = summary.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 = summary.CalculateLineCoverage(_module.Value).Percent; - double branchPercent = summary.CalculateBranchCoverage(_module.Value).Percent; - double methodPercent = summary.CalculateMethodCoverage(_module.Value).Percent; - - coverageTable.AddRow(Path.GetFileNameWithoutExtension(_module.Key), $"{InvariantFormat(linePercent)}%", $"{InvariantFormat(branchPercent)}%", $"{InvariantFormat(methodPercent)}%"); - } - - logger.LogInformation(coverageTable.ToStringAlternative()); - - coverageTable.Columns.Clear(); - coverageTable.Rows.Clear(); - - coverageTable.AddColumn(new[] { "", "Line", "Branch", "Method" }); - coverageTable.AddRow("Total", $"{InvariantFormat(totalLinePercent)}%", $"{InvariantFormat(totalBranchPercent)}%", $"{InvariantFormat(totalMethodPercent)}%"); - coverageTable.AddRow("Average", $"{InvariantFormat(averageLinePercent)}%", $"{InvariantFormat(averageBranchPercent)}%", $"{InvariantFormat(averageMethodPercent)}%"); - - logger.LogInformation(coverageTable.ToStringAlternative()); - if (process.ExitCode > 0) - { - exitCode += (int)CommandExitCodes.TestFailed; - } - - ThresholdTypeFlags thresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStat); - if (thresholdTypeFlags != ThresholdTypeFlags.None) - { - exitCode += (int)CommandExitCodes.CoverageBelowThreshold; - var exceptionMessageBuilder = new StringBuilder(); - if ((thresholdTypeFlags & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None) - { - exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} line coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Line]}"); - } - - if ((thresholdTypeFlags & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None) - { - exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} branch coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Branch]}"); - } - - if ((thresholdTypeFlags & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None) - { - exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} method coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Method]}"); - } - throw new Exception(exceptionMessageBuilder.ToString()); - } - - return Task.FromResult(exitCode); + string dOutput = output != null ? output : Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar.ToString(); + logger.LogInformation("\nCalculating coverage result..."); - } + CoverageResult result = coverage.GetCoverageResult(); + + string directory = Path.GetDirectoryName(dOutput); + if (directory == string.Empty) + { + directory = Directory.GetCurrentDirectory(); + } + else if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + foreach (string format in formats) + { + IReporter reporter = new ReporterFactory(format).CreateReporter(); + if (reporter == null) + { + throw new Exception($"Specified output format '{format}' is not supported"); + } + + if (reporter.OutputType == ReporterOutputType.Console) + { + // Output to console + logger.LogInformation(" Outputting results to console", important: true); + logger.LogInformation(reporter.Report(result, sourceRootTranslator), important: true); + } + else + { + // Output to file + string filename = Path.GetFileName(dOutput); + filename = (filename == string.Empty) ? $"coverage.{reporter.Extension}" : filename; + filename = Path.HasExtension(filename) ? filename : $"{filename}.{reporter.Extension}"; + + string report = Path.Combine(directory, filename); + logger.LogInformation($" Generating report '{report}'", important: true); + fileSystem.WriteAllText(report, reporter.Report(result, sourceRootTranslator)); + } + } + + var thresholdTypeFlagQueue = new Queue(); - catch (Win32Exception we) when (we.Source == "System.Diagnostics.Process") + foreach (string thresholdType in thresholdTypes) + { + if (thresholdType.Equals("line", StringComparison.OrdinalIgnoreCase)) + { + thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Line); + } + else if (thresholdType.Equals("branch", StringComparison.OrdinalIgnoreCase)) + { + thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Branch); + } + else if (thresholdType.Equals("method", StringComparison.OrdinalIgnoreCase)) + { + thresholdTypeFlagQueue.Enqueue(ThresholdTypeFlags.Method); + } + } + + var thresholdTypeFlagValues = new Dictionary(); + if (!string.IsNullOrEmpty(threshold) && threshold.Contains(',')) + { + IEnumerable thresholdValues = threshold.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()); + if (thresholdValues.Count() != thresholdTypeFlagQueue.Count) + { + throw new Exception($"Threshold type flag count ({thresholdTypeFlagQueue.Count}) and values count ({thresholdValues.Count()}) doesn't match"); + } + + foreach (string thresholdValue in thresholdValues) + { + if (double.TryParse(thresholdValue, out double value)) { - logger.LogError($"Start process '{target}' failed with '{we.Message}'"); - return Task.FromResult(exitCode > 0 ? exitCode : (int)CommandExitCodes.Exception); + thresholdTypeFlagValues[thresholdTypeFlagQueue.Dequeue()] = value; } - catch (Exception ex) + else { - logger.LogError(ex.Message); - return Task.FromResult(exitCode > 0 ? exitCode : (int)CommandExitCodes.Exception); + throw new Exception($"Invalid threshold value must be numeric"); } + } + } + else + { + double thresholdValue = threshold != null ? double.Parse(threshold) : 0; + + while (thresholdTypeFlagQueue.Any()) + { + thresholdTypeFlagValues[thresholdTypeFlagQueue.Dequeue()] = thresholdValue; + } + } + + var coverageTable = new ConsoleTable("Module", "Line", "Branch", "Method"); + var summary = new CoverageSummary(); + + CoverageDetails linePercentCalculation = summary.CalculateLineCoverage(result.Modules); + CoverageDetails branchPercentCalculation = summary.CalculateBranchCoverage(result.Modules); + CoverageDetails methodPercentCalculation = summary.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 = summary.CalculateLineCoverage(_module.Value).Percent; + double branchPercent = summary.CalculateBranchCoverage(_module.Value).Percent; + double methodPercent = summary.CalculateMethodCoverage(_module.Value).Percent; + coverageTable.AddRow(Path.GetFileNameWithoutExtension(_module.Key), $"{InvariantFormat(linePercent)}%", $"{InvariantFormat(branchPercent)}%", $"{InvariantFormat(methodPercent)}%"); } - static string InvariantFormat(double value) => value.ToString(CultureInfo.InvariantCulture); + logger.LogInformation(coverageTable.ToStringAlternative()); + + coverageTable.Columns.Clear(); + coverageTable.Rows.Clear(); + + coverageTable.AddColumn(new[] { "", "Line", "Branch", "Method" }); + coverageTable.AddRow("Total", $"{InvariantFormat(totalLinePercent)}%", $"{InvariantFormat(totalBranchPercent)}%", $"{InvariantFormat(totalMethodPercent)}%"); + coverageTable.AddRow("Average", $"{InvariantFormat(averageLinePercent)}%", $"{InvariantFormat(averageBranchPercent)}%", $"{InvariantFormat(averageMethodPercent)}%"); + + logger.LogInformation(coverageTable.ToStringAlternative()); + if (process.ExitCode > 0) + { + exitCode += (int)CommandExitCodes.TestFailed; + } + + ThresholdTypeFlags thresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStat); + if (thresholdTypeFlags != ThresholdTypeFlags.None) + { + exitCode += (int)CommandExitCodes.CoverageBelowThreshold; + var exceptionMessageBuilder = new StringBuilder(); + if ((thresholdTypeFlags & ThresholdTypeFlags.Line) != ThresholdTypeFlags.None) + { + exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} line coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Line]}"); + } + + if ((thresholdTypeFlags & ThresholdTypeFlags.Branch) != ThresholdTypeFlags.None) + { + exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} branch coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Branch]}"); + } + + if ((thresholdTypeFlags & ThresholdTypeFlags.Method) != ThresholdTypeFlags.None) + { + exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} method coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Method]}"); + } + throw new Exception(exceptionMessageBuilder.ToString()); + } + + return Task.FromResult(exitCode); + + + } + + catch (Win32Exception we) when (we.Source == "System.Diagnostics.Process") + { + logger.LogError($"Start process '{target}' failed with '{we.Message}'"); + return Task.FromResult(exitCode > 0 ? exitCode : (int)CommandExitCodes.Exception); + } + catch (Exception ex) + { + logger.LogError(ex.Message); + return Task.FromResult(exitCode > 0 ? exitCode : (int)CommandExitCodes.Exception); + } + } + + static string InvariantFormat(double value) => value.ToString(CultureInfo.InvariantCulture); + } } diff --git a/src/coverlet.console/Properties/AssemblyInfo.cs b/src/coverlet.console/Properties/AssemblyInfo.cs index 59db2a82f..aff16fa56 100644 --- a/src/coverlet.console/Properties/AssemblyInfo.cs +++ b/src/coverlet.console/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. -[assembly: System.Reflection.AssemblyKeyFileAttribute("coverlet.console.snk")] \ No newline at end of file +[assembly: System.Reflection.AssemblyKeyFileAttribute("coverlet.console.snk")] diff --git a/src/coverlet.core/Abstractions/IAssemblyAdapter.cs b/src/coverlet.core/Abstractions/IAssemblyAdapter.cs index 48b23084b..a020bced5 100644 --- a/src/coverlet.core/Abstractions/IAssemblyAdapter.cs +++ b/src/coverlet.core/Abstractions/IAssemblyAdapter.cs @@ -3,8 +3,8 @@ namespace Coverlet.Core.Abstractions { - internal interface IAssemblyAdapter - { - string GetAssemblyName(string assemblyPath); - } + internal interface IAssemblyAdapter + { + string GetAssemblyName(string assemblyPath); + } } diff --git a/src/coverlet.core/Abstractions/ICecilSymbolHelper.cs b/src/coverlet.core/Abstractions/ICecilSymbolHelper.cs index 06cdde3d1..b22463b5d 100644 --- a/src/coverlet.core/Abstractions/ICecilSymbolHelper.cs +++ b/src/coverlet.core/Abstractions/ICecilSymbolHelper.cs @@ -8,10 +8,10 @@ namespace Coverlet.Core.Abstractions { - internal interface ICecilSymbolHelper - { - IReadOnlyList GetBranchPoints(MethodDefinition methodDefinition); - bool SkipNotCoverableInstruction(MethodDefinition methodDefinition, Instruction instruction); - bool SkipInlineAssignedAutoProperty(bool skipAutoProps, MethodDefinition methodDefinition, Instruction instruction); - } + internal interface ICecilSymbolHelper + { + IReadOnlyList GetBranchPoints(MethodDefinition methodDefinition); + bool SkipNotCoverableInstruction(MethodDefinition methodDefinition, Instruction instruction); + bool SkipInlineAssignedAutoProperty(bool skipAutoProps, MethodDefinition methodDefinition, Instruction instruction); + } } diff --git a/src/coverlet.core/Abstractions/IConsole.cs b/src/coverlet.core/Abstractions/IConsole.cs index 72991cac5..2724ccf8b 100644 --- a/src/coverlet.core/Abstractions/IConsole.cs +++ b/src/coverlet.core/Abstractions/IConsole.cs @@ -3,8 +3,8 @@ namespace Coverlet.Core.Abstractions { - internal interface IConsole - { - public void WriteLine(string value); - } + internal interface IConsole + { + public void WriteLine(string value); + } } diff --git a/src/coverlet.core/Abstractions/IFileSystem.cs b/src/coverlet.core/Abstractions/IFileSystem.cs index cb710c758..6f0978467 100644 --- a/src/coverlet.core/Abstractions/IFileSystem.cs +++ b/src/coverlet.core/Abstractions/IFileSystem.cs @@ -5,24 +5,24 @@ namespace Coverlet.Core.Abstractions { - internal interface IFileSystem - { - bool Exists(string path); + internal interface IFileSystem + { + bool Exists(string path); - void WriteAllText(string path, string contents); + void WriteAllText(string path, string contents); - string ReadAllText(string path); + string ReadAllText(string path); - Stream OpenRead(string path); + Stream OpenRead(string path); - void Copy(string sourceFileName, string destFileName, bool overwrite); + void Copy(string sourceFileName, string destFileName, bool overwrite); - void Delete(string path); + void Delete(string path); - Stream NewFileStream(string path, FileMode mode); + Stream NewFileStream(string path, FileMode mode); - Stream NewFileStream(string path, FileMode mode, FileAccess access); + Stream NewFileStream(string path, FileMode mode, FileAccess access); - string[] ReadAllLines(string path); - } + string[] ReadAllLines(string path); + } } diff --git a/src/coverlet.core/Abstractions/IInstrumentationHelper.cs b/src/coverlet.core/Abstractions/IInstrumentationHelper.cs index 65af40011..fee916509 100644 --- a/src/coverlet.core/Abstractions/IInstrumentationHelper.cs +++ b/src/coverlet.core/Abstractions/IInstrumentationHelper.cs @@ -5,21 +5,21 @@ namespace Coverlet.Core.Abstractions { - internal interface IInstrumentationHelper - { - void BackupOriginalModule(string module, string identifier); - void DeleteHitsFile(string path); - string[] GetCoverableModules(string module, string[] directories, bool includeTestAssembly); - bool HasPdb(string module, out bool embedded); - bool IsModuleExcluded(string module, string[] excludeFilters); - bool IsModuleIncluded(string module, string[] includeFilters); - bool IsValidFilterExpression(string filter); - bool IsTypeExcluded(string module, string type, string[] excludeFilters); - bool IsTypeIncluded(string module, string type, string[] includeFilters); - void RestoreOriginalModule(string module, string identifier); - bool EmbeddedPortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources); - bool PortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources); - bool IsLocalMethod(string method); - void SetLogger(ILogger logger); - } + internal interface IInstrumentationHelper + { + void BackupOriginalModule(string module, string identifier); + void DeleteHitsFile(string path); + string[] GetCoverableModules(string module, string[] directories, bool includeTestAssembly); + bool HasPdb(string module, out bool embedded); + bool IsModuleExcluded(string module, string[] excludeFilters); + bool IsModuleIncluded(string module, string[] includeFilters); + bool IsValidFilterExpression(string filter); + bool IsTypeExcluded(string module, string type, string[] excludeFilters); + bool IsTypeIncluded(string module, string type, string[] includeFilters); + void RestoreOriginalModule(string module, string identifier); + bool EmbeddedPortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources); + bool PortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources); + bool IsLocalMethod(string method); + void SetLogger(ILogger logger); + } } diff --git a/src/coverlet.core/Abstractions/ILogger.cs b/src/coverlet.core/Abstractions/ILogger.cs index c3e6ef15e..c03831531 100644 --- a/src/coverlet.core/Abstractions/ILogger.cs +++ b/src/coverlet.core/Abstractions/ILogger.cs @@ -1,16 +1,16 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; namespace Coverlet.Core.Abstractions { - internal interface ILogger - { - void LogVerbose(string message); - void LogInformation(string message, bool important = false); - void LogWarning(string message); - void LogError(string message); - void LogError(Exception exception); - } -} \ No newline at end of file + internal interface ILogger + { + void LogVerbose(string message); + void LogInformation(string message, bool important = false); + void LogWarning(string message); + void LogError(string message); + void LogError(Exception exception); + } +} diff --git a/src/coverlet.core/Abstractions/IProcessExitHandler.cs b/src/coverlet.core/Abstractions/IProcessExitHandler.cs index 635015946..c0f9d524c 100644 --- a/src/coverlet.core/Abstractions/IProcessExitHandler.cs +++ b/src/coverlet.core/Abstractions/IProcessExitHandler.cs @@ -5,8 +5,8 @@ namespace Coverlet.Core.Abstractions { - internal interface IProcessExitHandler - { - void Add(EventHandler handler); - } + internal interface IProcessExitHandler + { + void Add(EventHandler handler); + } } diff --git a/src/coverlet.core/Abstractions/IReporter.cs b/src/coverlet.core/Abstractions/IReporter.cs index 9e497d62a..82441aa37 100644 --- a/src/coverlet.core/Abstractions/IReporter.cs +++ b/src/coverlet.core/Abstractions/IReporter.cs @@ -1,19 +1,19 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Coverlet.Core.Abstractions { - internal interface IReporter - { - ReporterOutputType OutputType { get; } - string Format { get; } - string Extension { get; } - string Report(CoverageResult result, ISourceRootTranslator sourceRootTranslator); - } + internal interface IReporter + { + ReporterOutputType OutputType { get; } + string Format { get; } + string Extension { get; } + string Report(CoverageResult result, ISourceRootTranslator sourceRootTranslator); + } - internal enum ReporterOutputType - { - File, - Console, - } -} \ No newline at end of file + internal enum ReporterOutputType + { + File, + Console, + } +} diff --git a/src/coverlet.core/Abstractions/IRetryHelper.cs b/src/coverlet.core/Abstractions/IRetryHelper.cs index 88a1b29d5..85f8b53bb 100644 --- a/src/coverlet.core/Abstractions/IRetryHelper.cs +++ b/src/coverlet.core/Abstractions/IRetryHelper.cs @@ -5,9 +5,9 @@ namespace Coverlet.Core.Abstractions { - internal interface IRetryHelper - { - void Retry(Action action, Func backoffStrategy, int maxAttemptCount = 3); - T Do(Func action, Func backoffStrategy, int maxAttemptCount = 3); - } + internal interface IRetryHelper + { + void Retry(Action action, Func backoffStrategy, int maxAttemptCount = 3); + T Do(Func action, Func backoffStrategy, int maxAttemptCount = 3); + } } diff --git a/src/coverlet.core/Abstractions/ISourceRootTranslator.cs b/src/coverlet.core/Abstractions/ISourceRootTranslator.cs index 4af6cfddf..2bbef7a87 100644 --- a/src/coverlet.core/Abstractions/ISourceRootTranslator.cs +++ b/src/coverlet.core/Abstractions/ISourceRootTranslator.cs @@ -6,11 +6,11 @@ namespace Coverlet.Core.Abstractions { - internal interface ISourceRootTranslator - { - bool AddMappingInCache(string originalFileName, string targetFileName); - string ResolveFilePath(string originalFileName); - string ResolveDeterministicPath(string originalFileName); - IReadOnlyList ResolvePathRoot(string pathRoot); - } + internal interface ISourceRootTranslator + { + bool AddMappingInCache(string originalFileName, string targetFileName); + string ResolveFilePath(string originalFileName); + string ResolveDeterministicPath(string originalFileName); + IReadOnlyList ResolvePathRoot(string pathRoot); + } } diff --git a/src/coverlet.core/Attributes/DoesNotReturnAttribute.cs b/src/coverlet.core/Attributes/DoesNotReturnAttribute.cs index cebe198f1..074d0a59f 100644 --- a/src/coverlet.core/Attributes/DoesNotReturnAttribute.cs +++ b/src/coverlet.core/Attributes/DoesNotReturnAttribute.cs @@ -5,6 +5,6 @@ namespace Coverlet.Core.Attributes { - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class)] - internal class DoesNotReturnAttribute : Attribute { } + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class)] + internal class DoesNotReturnAttribute : Attribute { } } diff --git a/src/coverlet.core/Attributes/ExcludeFromCoverage.cs b/src/coverlet.core/Attributes/ExcludeFromCoverage.cs index 71efcb7bc..1436c2175 100644 --- a/src/coverlet.core/Attributes/ExcludeFromCoverage.cs +++ b/src/coverlet.core/Attributes/ExcludeFromCoverage.cs @@ -1,10 +1,10 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; namespace Coverlet.Core.Attributes { - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class)] - internal sealed class ExcludeFromCoverageAttribute : Attribute { } -} \ No newline at end of file + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class)] + internal sealed class ExcludeFromCoverageAttribute : Attribute { } +} diff --git a/src/coverlet.core/CoverageDetails.cs b/src/coverlet.core/CoverageDetails.cs index 59db863c2..0359fa11a 100644 --- a/src/coverlet.core/CoverageDetails.cs +++ b/src/coverlet.core/CoverageDetails.cs @@ -1,30 +1,30 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; namespace Coverlet.Core { - internal class CoverageDetails - { - private double _averageModulePercent; + internal class CoverageDetails + { + private double _averageModulePercent; - public Modules Modules { get; internal set; } - public double Covered { get; internal set; } - public int Total { get; internal set; } - public double Percent - { - get - { - if (Modules?.Count == 0) return 0; - return Total == 0 ? 100D : Math.Floor((Covered / Total) * 10000) / 100; - } - } + public Modules Modules { get; internal set; } + public double Covered { get; internal set; } + public int Total { get; internal set; } + public double Percent + { + get + { + if (Modules?.Count == 0) return 0; + return Total == 0 ? 100D : Math.Floor((Covered / Total) * 10000) / 100; + } + } - public double AverageModulePercent - { - get { return Math.Floor(_averageModulePercent * 100) / 100; } - internal set { _averageModulePercent = value; } - } + public double AverageModulePercent + { + get { return Math.Floor(_averageModulePercent * 100) / 100; } + internal set { _averageModulePercent = value; } } -} \ No newline at end of file + } +} diff --git a/src/coverlet.core/CoveragePrepareResult.cs b/src/coverlet.core/CoveragePrepareResult.cs index 7c15d76c3..ecfd234cf 100644 --- a/src/coverlet.core/CoveragePrepareResult.cs +++ b/src/coverlet.core/CoveragePrepareResult.cs @@ -7,36 +7,36 @@ namespace Coverlet.Core { - // Followed safe serializer guide, will emit xml format - // https://docs.microsoft.com/en-us/visualstudio/code-quality/ca2300-do-not-use-insecure-deserializer-binaryformatter?view=vs-2019 - // https://docs.microsoft.com/en-us/visualstudio/code-quality/ca2301-do-not-call-binaryformatter-deserialize-without-first-setting-binaryformatter-binder?view=vs-2019 - [DataContract] - internal class CoveragePrepareResult - { - [DataMember] - public string Identifier { get; set; } - [DataMember] - public string ModuleOrDirectory { get; set; } - [DataMember] - public string MergeWith { get; set; } - [DataMember] - public bool UseSourceLink { get; set; } - [DataMember] - public InstrumenterResult[] Results { get; set; } - [DataMember] - public CoverageParameters Parameters { get; set; } + // Followed safe serializer guide, will emit xml format + // https://docs.microsoft.com/en-us/visualstudio/code-quality/ca2300-do-not-use-insecure-deserializer-binaryformatter?view=vs-2019 + // https://docs.microsoft.com/en-us/visualstudio/code-quality/ca2301-do-not-call-binaryformatter-deserialize-without-first-setting-binaryformatter-binder?view=vs-2019 + [DataContract] + internal class CoveragePrepareResult + { + [DataMember] + public string Identifier { get; set; } + [DataMember] + public string ModuleOrDirectory { get; set; } + [DataMember] + public string MergeWith { get; set; } + [DataMember] + public bool UseSourceLink { get; set; } + [DataMember] + public InstrumenterResult[] Results { get; set; } + [DataMember] + public CoverageParameters Parameters { get; set; } - public static CoveragePrepareResult Deserialize(Stream serializedInstrumentState) - { - return (CoveragePrepareResult)new DataContractSerializer(typeof(CoveragePrepareResult)).ReadObject(serializedInstrumentState); - } + public static CoveragePrepareResult Deserialize(Stream serializedInstrumentState) + { + return (CoveragePrepareResult)new DataContractSerializer(typeof(CoveragePrepareResult)).ReadObject(serializedInstrumentState); + } - public static Stream Serialize(CoveragePrepareResult instrumentState) - { - var ms = new MemoryStream(); - new DataContractSerializer(typeof(CoveragePrepareResult)).WriteObject(ms, instrumentState); - ms.Position = 0; - return ms; - } + public static Stream Serialize(CoveragePrepareResult instrumentState) + { + var ms = new MemoryStream(); + new DataContractSerializer(typeof(CoveragePrepareResult)).WriteObject(ms, instrumentState); + ms.Position = 0; + return ms; } + } } diff --git a/src/coverlet.core/CoverageSummary.cs b/src/coverlet.core/CoverageSummary.cs index a56cda454..34370074b 100644 --- a/src/coverlet.core/CoverageSummary.cs +++ b/src/coverlet.core/CoverageSummary.cs @@ -7,270 +7,270 @@ namespace Coverlet.Core { - internal class CoverageSummary + internal class CoverageSummary + { + public CoverageDetails CalculateLineCoverage(Lines lines) { - public CoverageDetails CalculateLineCoverage(Lines lines) - { - var details = new CoverageDetails(); - details.Covered = lines.Where(l => l.Value > 0).Count(); - details.Total = lines.Count; - return details; - } + var details = new CoverageDetails(); + details.Covered = lines.Where(l => l.Value > 0).Count(); + details.Total = lines.Count; + return details; + } - public CoverageDetails CalculateLineCoverage(Methods methods) - { - var details = new CoverageDetails(); - foreach (KeyValuePair method in methods) - { - CoverageDetails methodCoverage = CalculateLineCoverage(method.Value.Lines); - details.Covered += methodCoverage.Covered; - details.Total += methodCoverage.Total; - } - return details; - } + public CoverageDetails CalculateLineCoverage(Methods methods) + { + var details = new CoverageDetails(); + foreach (KeyValuePair method in methods) + { + CoverageDetails methodCoverage = CalculateLineCoverage(method.Value.Lines); + details.Covered += methodCoverage.Covered; + details.Total += methodCoverage.Total; + } + return details; + } - public CoverageDetails CalculateLineCoverage(Classes classes) - { - var details = new CoverageDetails(); - foreach (KeyValuePair @class in classes) - { - CoverageDetails classCoverage = CalculateLineCoverage(@class.Value); - details.Covered += classCoverage.Covered; - details.Total += classCoverage.Total; - } - return details; - } + public CoverageDetails CalculateLineCoverage(Classes classes) + { + var details = new CoverageDetails(); + foreach (KeyValuePair @class in classes) + { + CoverageDetails classCoverage = CalculateLineCoverage(@class.Value); + details.Covered += classCoverage.Covered; + details.Total += classCoverage.Total; + } + return details; + } - public CoverageDetails CalculateLineCoverage(Documents documents) - { - var details = new CoverageDetails(); - foreach (KeyValuePair document in documents) - { - CoverageDetails documentCoverage = CalculateLineCoverage(document.Value); - details.Covered += documentCoverage.Covered; - details.Total += documentCoverage.Total; - } - return details; - } + public CoverageDetails CalculateLineCoverage(Documents documents) + { + var details = new CoverageDetails(); + foreach (KeyValuePair document in documents) + { + CoverageDetails documentCoverage = CalculateLineCoverage(document.Value); + details.Covered += documentCoverage.Covered; + details.Total += documentCoverage.Total; + } + return details; + } - public CoverageDetails CalculateLineCoverage(Modules modules) - { - var details = new CoverageDetails { Modules = modules }; - double accumPercent = 0.0D; - - if (modules.Count == 0) - return details; - - foreach (KeyValuePair module in modules) - { - CoverageDetails moduleCoverage = CalculateLineCoverage(module.Value); - details.Covered += moduleCoverage.Covered; - details.Total += moduleCoverage.Total; - accumPercent += moduleCoverage.Percent; - } - details.AverageModulePercent = accumPercent / modules.Count; - return details; - } + public CoverageDetails CalculateLineCoverage(Modules modules) + { + var details = new CoverageDetails { Modules = modules }; + double accumPercent = 0.0D; + + if (modules.Count == 0) + return details; + + foreach (KeyValuePair module in modules) + { + CoverageDetails moduleCoverage = CalculateLineCoverage(module.Value); + details.Covered += moduleCoverage.Covered; + details.Total += moduleCoverage.Total; + accumPercent += moduleCoverage.Percent; + } + details.AverageModulePercent = accumPercent / modules.Count; + return details; + } - public CoverageDetails CalculateBranchCoverage(IList branches) - { - var details = new CoverageDetails(); - details.Covered = branches.Count(bi => bi.Hits > 0); - details.Total = branches.Count; - return details; - } + public CoverageDetails CalculateBranchCoverage(IList branches) + { + var details = new CoverageDetails(); + details.Covered = branches.Count(bi => bi.Hits > 0); + details.Total = branches.Count; + return details; + } - public int CalculateNpathComplexity(IList branches) + public int CalculateNpathComplexity(IList branches) + { + // Adapted from OpenCover see https://github.com/OpenCover/opencover/blob/master/main/OpenCover.Framework/Persistance/BasePersistance.cs#L419 + if (!branches.Any()) + { + return 0; + } + + var paths = new Dictionary(); + foreach (BranchInfo branch in branches) + { + if (!paths.TryGetValue(branch.Offset, out int count)) { - // Adapted from OpenCover see https://github.com/OpenCover/opencover/blob/master/main/OpenCover.Framework/Persistance/BasePersistance.cs#L419 - if (!branches.Any()) - { - return 0; - } - - var paths = new Dictionary(); - foreach (BranchInfo branch in branches) - { - if (!paths.TryGetValue(branch.Offset, out int count)) - { - count = 0; - } - paths[branch.Offset] = ++count; - } - - int npath = 1; - foreach (int branchPoints in paths.Values) - { - try - { - npath = checked(npath * branchPoints); - } - catch (OverflowException) - { - npath = int.MaxValue; - break; - } - } - return npath; + count = 0; } + paths[branch.Offset] = ++count; + } - public int CalculateCyclomaticComplexity(IList branches) + int npath = 1; + foreach (int branchPoints in paths.Values) + { + try { - return Math.Max(1, branches.Count); + npath = checked(npath * branchPoints); } - - public int CalculateCyclomaticComplexity(Methods methods) + catch (OverflowException) { - return methods.Values.Select(m => CalculateCyclomaticComplexity(m.Branches)).Sum(); + npath = int.MaxValue; + break; } + } + return npath; + } - public int CalculateMaxCyclomaticComplexity(Methods methods) - { - return methods.Values.Select(m => CalculateCyclomaticComplexity(m.Branches)).DefaultIfEmpty(1).Max(); - } + public int CalculateCyclomaticComplexity(IList branches) + { + return Math.Max(1, branches.Count); + } - public int CalculateMinCyclomaticComplexity(Methods methods) - { - return methods.Values.Select(m => CalculateCyclomaticComplexity(m.Branches)).DefaultIfEmpty(1).Min(); - } + public int CalculateCyclomaticComplexity(Methods methods) + { + return methods.Values.Select(m => CalculateCyclomaticComplexity(m.Branches)).Sum(); + } - public int CalculateCyclomaticComplexity(Modules modules) - { - return modules.Values.Select(CalculateCyclomaticComplexity).Sum(); - } + public int CalculateMaxCyclomaticComplexity(Methods methods) + { + return methods.Values.Select(m => CalculateCyclomaticComplexity(m.Branches)).DefaultIfEmpty(1).Max(); + } - public int CalculateMaxCyclomaticComplexity(Modules modules) - { - return modules.Values.Select(CalculateCyclomaticComplexity).DefaultIfEmpty(1).Max(); - } + public int CalculateMinCyclomaticComplexity(Methods methods) + { + return methods.Values.Select(m => CalculateCyclomaticComplexity(m.Branches)).DefaultIfEmpty(1).Min(); + } - public int CalculateMinCyclomaticComplexity(Modules modules) - { - return modules.Values.Select(CalculateCyclomaticComplexity).DefaultIfEmpty(1).Min(); - } + public int CalculateCyclomaticComplexity(Modules modules) + { + return modules.Values.Select(CalculateCyclomaticComplexity).Sum(); + } - public int CalculateCyclomaticComplexity(Documents documents) - { - return documents.Values.SelectMany(c => c.Values.Select(CalculateCyclomaticComplexity)).Sum(); - } + public int CalculateMaxCyclomaticComplexity(Modules modules) + { + return modules.Values.Select(CalculateCyclomaticComplexity).DefaultIfEmpty(1).Max(); + } - public CoverageDetails CalculateBranchCoverage(Methods methods) - { - var details = new CoverageDetails(); - foreach (KeyValuePair method in methods) - { - CoverageDetails methodCoverage = CalculateBranchCoverage(method.Value.Branches); - details.Covered += methodCoverage.Covered; - details.Total += methodCoverage.Total; - } - return details; - } + public int CalculateMinCyclomaticComplexity(Modules modules) + { + return modules.Values.Select(CalculateCyclomaticComplexity).DefaultIfEmpty(1).Min(); + } - public CoverageDetails CalculateBranchCoverage(Classes classes) - { - var details = new CoverageDetails(); - foreach (KeyValuePair @class in classes) - { - CoverageDetails classCoverage = CalculateBranchCoverage(@class.Value); - details.Covered += classCoverage.Covered; - details.Total += classCoverage.Total; - } - return details; - } + public int CalculateCyclomaticComplexity(Documents documents) + { + return documents.Values.SelectMany(c => c.Values.Select(CalculateCyclomaticComplexity)).Sum(); + } - public CoverageDetails CalculateBranchCoverage(Documents documents) - { - var details = new CoverageDetails(); - foreach (KeyValuePair document in documents) - { - CoverageDetails documentCoverage = CalculateBranchCoverage(document.Value); - details.Covered += documentCoverage.Covered; - details.Total += documentCoverage.Total; - } - return details; - } + public CoverageDetails CalculateBranchCoverage(Methods methods) + { + var details = new CoverageDetails(); + foreach (KeyValuePair method in methods) + { + CoverageDetails methodCoverage = CalculateBranchCoverage(method.Value.Branches); + details.Covered += methodCoverage.Covered; + details.Total += methodCoverage.Total; + } + return details; + } - public CoverageDetails CalculateBranchCoverage(Modules modules) - { - var details = new CoverageDetails { Modules = modules }; - double accumPercent = 0.0D; - - if (modules.Count == 0) - return details; - - foreach (KeyValuePair module in modules) - { - CoverageDetails moduleCoverage = CalculateBranchCoverage(module.Value); - details.Covered += moduleCoverage.Covered; - details.Total += moduleCoverage.Total; - accumPercent += moduleCoverage.Percent; - } - details.AverageModulePercent = modules.Count == 0 ? 0 : accumPercent / modules.Count; - return details; - } + public CoverageDetails CalculateBranchCoverage(Classes classes) + { + var details = new CoverageDetails(); + foreach (KeyValuePair @class in classes) + { + CoverageDetails classCoverage = CalculateBranchCoverage(@class.Value); + details.Covered += classCoverage.Covered; + details.Total += classCoverage.Total; + } + return details; + } - public CoverageDetails CalculateMethodCoverage(Lines lines) - { - var details = new CoverageDetails(); - details.Covered = lines.Any(l => l.Value > 0) ? 1 : 0; - details.Total = 1; - return details; - } + public CoverageDetails CalculateBranchCoverage(Documents documents) + { + var details = new CoverageDetails(); + foreach (KeyValuePair document in documents) + { + CoverageDetails documentCoverage = CalculateBranchCoverage(document.Value); + details.Covered += documentCoverage.Covered; + details.Total += documentCoverage.Total; + } + return details; + } - public CoverageDetails CalculateMethodCoverage(Methods methods) - { - var details = new CoverageDetails(); - IEnumerable> methodsWithLines = methods.Where(m => m.Value.Lines.Count > 0); - foreach (KeyValuePair method in methodsWithLines) - { - CoverageDetails methodCoverage = CalculateMethodCoverage(method.Value.Lines); - details.Covered += methodCoverage.Covered; - } - details.Total = methodsWithLines.Count(); - return details; - } + public CoverageDetails CalculateBranchCoverage(Modules modules) + { + var details = new CoverageDetails { Modules = modules }; + double accumPercent = 0.0D; + + if (modules.Count == 0) + return details; + + foreach (KeyValuePair module in modules) + { + CoverageDetails moduleCoverage = CalculateBranchCoverage(module.Value); + details.Covered += moduleCoverage.Covered; + details.Total += moduleCoverage.Total; + accumPercent += moduleCoverage.Percent; + } + details.AverageModulePercent = modules.Count == 0 ? 0 : accumPercent / modules.Count; + return details; + } - public CoverageDetails CalculateMethodCoverage(Classes classes) - { - var details = new CoverageDetails(); - foreach (KeyValuePair @class in classes) - { - CoverageDetails classCoverage = CalculateMethodCoverage(@class.Value); - details.Covered += classCoverage.Covered; - details.Total += classCoverage.Total; - } - return details; - } + public CoverageDetails CalculateMethodCoverage(Lines lines) + { + var details = new CoverageDetails(); + details.Covered = lines.Any(l => l.Value > 0) ? 1 : 0; + details.Total = 1; + return details; + } - public CoverageDetails CalculateMethodCoverage(Documents documents) - { - var details = new CoverageDetails(); - foreach (KeyValuePair document in documents) - { - CoverageDetails documentCoverage = CalculateMethodCoverage(document.Value); - details.Covered += documentCoverage.Covered; - details.Total += documentCoverage.Total; - } - return details; - } + public CoverageDetails CalculateMethodCoverage(Methods methods) + { + var details = new CoverageDetails(); + IEnumerable> methodsWithLines = methods.Where(m => m.Value.Lines.Count > 0); + foreach (KeyValuePair method in methodsWithLines) + { + CoverageDetails methodCoverage = CalculateMethodCoverage(method.Value.Lines); + details.Covered += methodCoverage.Covered; + } + details.Total = methodsWithLines.Count(); + return details; + } - public CoverageDetails CalculateMethodCoverage(Modules modules) - { - var details = new CoverageDetails { Modules = modules }; - double accumPercent = 0.0D; - - if (modules.Count == 0) - return details; - - foreach (KeyValuePair module in modules) - { - CoverageDetails moduleCoverage = CalculateMethodCoverage(module.Value); - details.Covered += moduleCoverage.Covered; - details.Total += moduleCoverage.Total; - accumPercent += moduleCoverage.Percent; - } - details.AverageModulePercent = modules.Count == 0 ? 0 : accumPercent / modules.Count; - return details; - } + public CoverageDetails CalculateMethodCoverage(Classes classes) + { + var details = new CoverageDetails(); + foreach (KeyValuePair @class in classes) + { + CoverageDetails classCoverage = CalculateMethodCoverage(@class.Value); + details.Covered += classCoverage.Covered; + details.Total += classCoverage.Total; + } + return details; + } + + public CoverageDetails CalculateMethodCoverage(Documents documents) + { + var details = new CoverageDetails(); + foreach (KeyValuePair document in documents) + { + CoverageDetails documentCoverage = CalculateMethodCoverage(document.Value); + details.Covered += documentCoverage.Covered; + details.Total += documentCoverage.Total; + } + return details; + } + + public CoverageDetails CalculateMethodCoverage(Modules modules) + { + var details = new CoverageDetails { Modules = modules }; + double accumPercent = 0.0D; + + if (modules.Count == 0) + return details; + + foreach (KeyValuePair module in modules) + { + CoverageDetails moduleCoverage = CalculateMethodCoverage(module.Value); + details.Covered += moduleCoverage.Covered; + details.Total += moduleCoverage.Total; + accumPercent += moduleCoverage.Percent; + } + details.AverageModulePercent = modules.Count == 0 ? 0 : accumPercent / modules.Count; + return details; } + } } diff --git a/src/coverlet.core/Enums/AssemblySearchType.cs b/src/coverlet.core/Enums/AssemblySearchType.cs index 099e54217..4eef8b96f 100644 --- a/src/coverlet.core/Enums/AssemblySearchType.cs +++ b/src/coverlet.core/Enums/AssemblySearchType.cs @@ -3,10 +3,10 @@ namespace Coverlet.Core.Enums { - internal enum AssemblySearchType - { - MissingAny, - MissingAll, - None - } + internal enum AssemblySearchType + { + MissingAny, + MissingAll, + None + } } diff --git a/src/coverlet.core/Enums/ThresholdStatistic.cs b/src/coverlet.core/Enums/ThresholdStatistic.cs index 1dbb55dc0..31aa72cd9 100644 --- a/src/coverlet.core/Enums/ThresholdStatistic.cs +++ b/src/coverlet.core/Enums/ThresholdStatistic.cs @@ -1,12 +1,12 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Coverlet.Core.Enums { - internal enum ThresholdStatistic - { - Minimum, - Average, - Total - } -} \ No newline at end of file + internal enum ThresholdStatistic + { + Minimum, + Average, + Total + } +} diff --git a/src/coverlet.core/Enums/ThresholdTypeFlags.cs b/src/coverlet.core/Enums/ThresholdTypeFlags.cs index 9b24222a5..2463fab9a 100644 --- a/src/coverlet.core/Enums/ThresholdTypeFlags.cs +++ b/src/coverlet.core/Enums/ThresholdTypeFlags.cs @@ -1,16 +1,16 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; namespace Coverlet.Core.Enums { - [Flags] - internal enum ThresholdTypeFlags - { - None = 0, - Line = 2, - Branch = 4, - Method = 8 - } -} \ No newline at end of file + [Flags] + internal enum ThresholdTypeFlags + { + None = 0, + Line = 2, + Branch = 4, + Method = 8 + } +} diff --git a/src/coverlet.core/Exceptions.cs b/src/coverlet.core/Exceptions.cs index 8b97ca283..d65b22096 100644 --- a/src/coverlet.core/Exceptions.cs +++ b/src/coverlet.core/Exceptions.cs @@ -5,25 +5,25 @@ namespace Coverlet.Core.Exceptions { - [Serializable] - public class CoverletException : Exception - { - public CoverletException() { } - public CoverletException(string message) : base(message) { } - public CoverletException(string message, System.Exception inner) : base(message, inner) { } - protected CoverletException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } + [Serializable] + public class CoverletException : Exception + { + public CoverletException() { } + public CoverletException(string message) : base(message) { } + public CoverletException(string message, System.Exception inner) : base(message, inner) { } + protected CoverletException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } - [Serializable] - internal class CecilAssemblyResolutionException : CoverletException - { - public CecilAssemblyResolutionException() { } - public CecilAssemblyResolutionException(string message) : base(message) { } - public CecilAssemblyResolutionException(string message, System.Exception inner) : base(message, inner) { } - protected CecilAssemblyResolutionException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } + [Serializable] + internal class CecilAssemblyResolutionException : CoverletException + { + public CecilAssemblyResolutionException() { } + public CecilAssemblyResolutionException(string message) : base(message) { } + public CecilAssemblyResolutionException(string message, System.Exception inner) : base(message, inner) { } + protected CecilAssemblyResolutionException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } } diff --git a/src/coverlet.core/Extensions/HelperExtensions.cs b/src/coverlet.core/Extensions/HelperExtensions.cs index 30439c0c9..191e9cc38 100644 --- a/src/coverlet.core/Extensions/HelperExtensions.cs +++ b/src/coverlet.core/Extensions/HelperExtensions.cs @@ -6,13 +6,13 @@ namespace Coverlet.Core.Extensions { - internal static class HelperExtensions + internal static class HelperExtensions + { + [ExcludeFromCoverage] + public static TRet Maybe(this T value, Func action, TRet defValue = default) + where T : class { - [ExcludeFromCoverage] - public static TRet Maybe(this T value, Func action, TRet defValue = default) - where T : class - { - return (value != null) ? action(value) : defValue; - } + return (value != null) ? action(value) : defValue; } + } } diff --git a/src/coverlet.core/Helpers/AssemblyAdapter.cs b/src/coverlet.core/Helpers/AssemblyAdapter.cs index f4626d2ff..124c11a9f 100644 --- a/src/coverlet.core/Helpers/AssemblyAdapter.cs +++ b/src/coverlet.core/Helpers/AssemblyAdapter.cs @@ -6,11 +6,11 @@ namespace Coverlet.Core.Helpers { - internal class AssemblyAdapter : IAssemblyAdapter + internal class AssemblyAdapter : IAssemblyAdapter + { + public string GetAssemblyName(string assemblyPath) { - public string GetAssemblyName(string assemblyPath) - { - return AssemblyName.GetAssemblyName(assemblyPath).Name; - } + return AssemblyName.GetAssemblyName(assemblyPath).Name; } + } } diff --git a/src/coverlet.core/Helpers/Console.cs b/src/coverlet.core/Helpers/Console.cs index 8781b49de..4652a194a 100644 --- a/src/coverlet.core/Helpers/Console.cs +++ b/src/coverlet.core/Helpers/Console.cs @@ -6,11 +6,11 @@ namespace Coverlet.Core.Helpers { - public class SystemConsole : IConsole + public class SystemConsole : IConsole + { + public void WriteLine(string value) { - public void WriteLine(string value) - { - Console.WriteLine(value); - } + Console.WriteLine(value); } + } } diff --git a/src/coverlet.core/Helpers/FileSystem.cs b/src/coverlet.core/Helpers/FileSystem.cs index 10dfc8f0a..e8af5b947 100644 --- a/src/coverlet.core/Helpers/FileSystem.cs +++ b/src/coverlet.core/Helpers/FileSystem.cs @@ -6,61 +6,61 @@ namespace Coverlet.Core.Helpers { - internal class FileSystem : IFileSystem + internal class FileSystem : IFileSystem + { + // We need to partial mock this method on tests + public virtual bool Exists(string path) { - // We need to partial mock this method on tests - public virtual bool Exists(string path) - { - return File.Exists(path); - } + return File.Exists(path); + } - public void WriteAllText(string path, string contents) - { - File.WriteAllText(path, contents); - } + public void WriteAllText(string path, string contents) + { + File.WriteAllText(path, contents); + } - public string ReadAllText(string path) - { - return File.ReadAllText(path); - } + public string ReadAllText(string path) + { + return File.ReadAllText(path); + } - // We need to partial mock this method on tests - public virtual Stream OpenRead(string path) - { - return File.OpenRead(path); - } + // We need to partial mock this method on tests + public virtual Stream OpenRead(string path) + { + return File.OpenRead(path); + } - public void Copy(string sourceFileName, string destFileName, bool overwrite) - { - File.Copy(sourceFileName, destFileName, overwrite); - } + public void Copy(string sourceFileName, string destFileName, bool overwrite) + { + File.Copy(sourceFileName, destFileName, overwrite); + } - public void Delete(string path) - { - File.Delete(path); - } + public void Delete(string path) + { + File.Delete(path); + } - // We need to partial mock this method on tests - public virtual Stream NewFileStream(string path, FileMode mode) - { - return new FileStream(path, mode); - } + // We need to partial mock this method on tests + public virtual Stream NewFileStream(string path, FileMode mode) + { + return new FileStream(path, mode); + } - // We need to partial mock this method on tests - public virtual Stream NewFileStream(string path, FileMode mode, FileAccess access) - { - return new FileStream(path, mode, access); - } + // We need to partial mock this method on tests + public virtual Stream NewFileStream(string path, FileMode mode, FileAccess access) + { + return new FileStream(path, mode, access); + } - public string[] ReadAllLines(string path) - { - return File.ReadAllLines(path); - } + public string[] ReadAllLines(string path) + { + return File.ReadAllLines(path); + } - // Escape format characters in file names - internal static string EscapeFileName(string fileName) - { - return fileName?.Replace("{", "{{").Replace("}", "}}"); - } + // Escape format characters in file names + internal static string EscapeFileName(string fileName) + { + return fileName?.Replace("{", "{{").Replace("}", "}}"); } + } } diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs index 6723fc733..1221353e3 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -16,477 +16,477 @@ namespace Coverlet.Core.Helpers { - internal class InstrumentationHelper : IInstrumentationHelper + internal class InstrumentationHelper : IInstrumentationHelper + { + private const int RetryAttempts = 12; + private readonly ConcurrentDictionary _backupList = new(); + private readonly IRetryHelper _retryHelper; + private readonly IFileSystem _fileSystem; + private readonly ISourceRootTranslator _sourceRootTranslator; + private ILogger _logger; + + public InstrumentationHelper(IProcessExitHandler processExitHandler, IRetryHelper retryHelper, IFileSystem fileSystem, ILogger logger, ISourceRootTranslator sourceRootTranslator) { - private const int RetryAttempts = 12; - private readonly ConcurrentDictionary _backupList = new(); - private readonly IRetryHelper _retryHelper; - private readonly IFileSystem _fileSystem; - private readonly ISourceRootTranslator _sourceRootTranslator; - private ILogger _logger; - - public InstrumentationHelper(IProcessExitHandler processExitHandler, IRetryHelper retryHelper, IFileSystem fileSystem, ILogger logger, ISourceRootTranslator sourceRootTranslator) - { - processExitHandler.Add((s, e) => RestoreOriginalModules()); - _retryHelper = retryHelper; - _fileSystem = fileSystem; - _logger = logger; - _sourceRootTranslator = sourceRootTranslator; - } + processExitHandler.Add((s, e) => RestoreOriginalModules()); + _retryHelper = retryHelper; + _fileSystem = fileSystem; + _logger = logger; + _sourceRootTranslator = sourceRootTranslator; + } - public string[] GetCoverableModules(string moduleOrAppDirectory, string[] directories, bool includeTestAssembly) - { - Debug.Assert(directories != null); - Debug.Assert(moduleOrAppDirectory != null); + public string[] GetCoverableModules(string moduleOrAppDirectory, string[] directories, bool includeTestAssembly) + { + Debug.Assert(directories != null); + Debug.Assert(moduleOrAppDirectory != null); - bool isAppDirectory = !File.Exists(moduleOrAppDirectory) && Directory.Exists(moduleOrAppDirectory); - string moduleDirectory = isAppDirectory ? moduleOrAppDirectory : Path.GetDirectoryName(moduleOrAppDirectory); + bool isAppDirectory = !File.Exists(moduleOrAppDirectory) && Directory.Exists(moduleOrAppDirectory); + string moduleDirectory = isAppDirectory ? moduleOrAppDirectory : Path.GetDirectoryName(moduleOrAppDirectory); - if (moduleDirectory == string.Empty) - { - moduleDirectory = Directory.GetCurrentDirectory(); - } + if (moduleDirectory == string.Empty) + { + moduleDirectory = Directory.GetCurrentDirectory(); + } - var dirs = new List() + var dirs = new List() { // Add the test assembly's directory. moduleDirectory }; - // Prepare all the directories we probe for modules. - foreach (string directory in directories) - { - if (string.IsNullOrWhiteSpace(directory)) continue; + // Prepare all the directories we probe for modules. + foreach (string directory in directories) + { + if (string.IsNullOrWhiteSpace(directory)) continue; - string fullPath = (!Path.IsPathRooted(directory) - ? Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), directory)) - : directory).TrimEnd('*'); + string fullPath = (!Path.IsPathRooted(directory) + ? Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), directory)) + : directory).TrimEnd('*'); - if (!Directory.Exists(fullPath)) continue; + if (!Directory.Exists(fullPath)) continue; - if (directory.EndsWith("*", StringComparison.Ordinal)) - dirs.AddRange(Directory.GetDirectories(fullPath)); - else - dirs.Add(fullPath); - } + if (directory.EndsWith("*", StringComparison.Ordinal)) + dirs.AddRange(Directory.GetDirectories(fullPath)); + else + dirs.Add(fullPath); + } - // The module's name must be unique. - var uniqueModules = new HashSet(); + // The module's name must be unique. + var uniqueModules = new HashSet(); - if (!includeTestAssembly && !isAppDirectory) - uniqueModules.Add(Path.GetFileName(moduleOrAppDirectory)); + if (!includeTestAssembly && !isAppDirectory) + uniqueModules.Add(Path.GetFileName(moduleOrAppDirectory)); - return dirs.SelectMany(d => Directory.EnumerateFiles(d)) - .Where(m => IsAssembly(m) && uniqueModules.Add(Path.GetFileName(m))) - .ToArray(); - } + return dirs.SelectMany(d => Directory.EnumerateFiles(d)) + .Where(m => IsAssembly(m) && uniqueModules.Add(Path.GetFileName(m))) + .ToArray(); + } - public bool HasPdb(string module, out bool embedded) + public bool HasPdb(string module, out bool embedded) + { + embedded = false; + using Stream moduleStream = _fileSystem.OpenRead(module); + using var peReader = new PEReader(moduleStream); + foreach (DebugDirectoryEntry entry in peReader.ReadDebugDirectory()) + { + if (entry.Type == DebugDirectoryEntryType.CodeView) { + CodeViewDebugDirectoryData codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); + string modulePdbFileName = $"{Path.GetFileNameWithoutExtension(module)}.pdb"; + if (_sourceRootTranslator.ResolveFilePath(codeViewData.Path) == modulePdbFileName) + { + // PDB is embedded + embedded = true; + return true; + } + + if (_fileSystem.Exists(_sourceRootTranslator.ResolveFilePath(codeViewData.Path))) + { + // local PDB is located within original build location embedded = false; - using Stream moduleStream = _fileSystem.OpenRead(module); - using var peReader = new PEReader(moduleStream); - foreach (DebugDirectoryEntry entry in peReader.ReadDebugDirectory()) - { - if (entry.Type == DebugDirectoryEntryType.CodeView) - { - CodeViewDebugDirectoryData codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); - string modulePdbFileName = $"{Path.GetFileNameWithoutExtension(module)}.pdb"; - if (_sourceRootTranslator.ResolveFilePath(codeViewData.Path) == modulePdbFileName) - { - // PDB is embedded - embedded = true; - return true; - } - - if (_fileSystem.Exists(_sourceRootTranslator.ResolveFilePath(codeViewData.Path))) - { - // local PDB is located within original build location - embedded = false; - return true; - } - - string localPdbFileName = Path.Combine(Path.GetDirectoryName(module), modulePdbFileName); - if (_fileSystem.Exists(localPdbFileName)) - { - // local PDB is located within same folder as module - embedded = false; - - // mapping need to be registered in _sourceRootTranslator to use that discovery - _sourceRootTranslator.AddMappingInCache(codeViewData.Path, localPdbFileName); - - return true; - } - } - } + return true; + } - return false; - } + string localPdbFileName = Path.Combine(Path.GetDirectoryName(module), modulePdbFileName); + if (_fileSystem.Exists(localPdbFileName)) + { + // local PDB is located within same folder as module + embedded = false; + + // mapping need to be registered in _sourceRootTranslator to use that discovery + _sourceRootTranslator.AddMappingInCache(codeViewData.Path, localPdbFileName); - public bool EmbeddedPortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources) - { - using Stream moduleStream = _fileSystem.OpenRead(module); - using var peReader = new PEReader(moduleStream); - foreach (DebugDirectoryEntry entry in peReader.ReadDebugDirectory()) - { - if (entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb) - { - using MetadataReaderProvider embeddedMetadataProvider = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(entry); - MetadataReader metadataReader = embeddedMetadataProvider.GetMetadataReader(); - - if (!MatchDocumentsWithSources(module, excludeAssembliesWithoutSources, metadataReader)) - { - return false; - } - } - } - - // If we don't have EmbeddedPortablePdb entry return true, for instance empty dll - // We should call this method only on embedded pdb module return true; + } } + } - public bool PortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources) + return false; + } + + public bool EmbeddedPortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources) + { + using Stream moduleStream = _fileSystem.OpenRead(module); + using var peReader = new PEReader(moduleStream); + foreach (DebugDirectoryEntry entry in peReader.ReadDebugDirectory()) + { + if (entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb) { - using Stream moduleStream = _fileSystem.OpenRead(module); - using var peReader = new PEReader(moduleStream); - foreach (DebugDirectoryEntry entry in peReader.ReadDebugDirectory()) - { - if (entry.Type == DebugDirectoryEntryType.CodeView) - { - CodeViewDebugDirectoryData codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); - using Stream pdbStream = _fileSystem.OpenRead(_sourceRootTranslator.ResolveFilePath(codeViewData.Path)); - using var metadataReaderProvider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); - MetadataReader metadataReader = null; - try - { - metadataReader = metadataReaderProvider.GetMetadataReader(); - } - catch (BadImageFormatException) - { - _logger.LogWarning($"{nameof(BadImageFormatException)} during MetadataReaderProvider.FromPortablePdbStream in InstrumentationHelper.PortablePdbHasLocalSource, unable to check if module has got local source."); - return true; - } - - if (!MatchDocumentsWithSources(module, excludeAssembliesWithoutSources, metadataReader)) - { - return false; - } - } - } + using MetadataReaderProvider embeddedMetadataProvider = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(entry); + MetadataReader metadataReader = embeddedMetadataProvider.GetMetadataReader(); - return true; + if (!MatchDocumentsWithSources(module, excludeAssembliesWithoutSources, metadataReader)) + { + return false; + } } + } - private bool MatchDocumentsWithSources(string module, AssemblySearchType excludeAssembliesWithoutSources, - MetadataReader metadataReader) - { - if (excludeAssembliesWithoutSources.Equals(AssemblySearchType.MissingAll)) - { - bool anyDocumentMatches = MatchDocumentsWithSourcesMissingAll(metadataReader); - if (!anyDocumentMatches) - { - _logger.LogVerbose($"Excluding module from instrumentation: {module}, pdb without any local source files"); - return false; - } - } - - if (excludeAssembliesWithoutSources.Equals(AssemblySearchType.MissingAny)) - { - (bool allDocumentsMatch, string notFoundDocument) = MatchDocumentsWithSourcesMissingAny(metadataReader); - - if (!allDocumentsMatch) - { - _logger.LogVerbose( - $"Excluding module from instrumentation: {module}, pdb without local source files, [{FileSystem.EscapeFileName(notFoundDocument)}]"); - return false; - } - } + // If we don't have EmbeddedPortablePdb entry return true, for instance empty dll + // We should call this method only on embedded pdb module + return true; + } + public bool PortablePdbHasLocalSource(string module, AssemblySearchType excludeAssembliesWithoutSources) + { + using Stream moduleStream = _fileSystem.OpenRead(module); + using var peReader = new PEReader(moduleStream); + foreach (DebugDirectoryEntry entry in peReader.ReadDebugDirectory()) + { + if (entry.Type == DebugDirectoryEntryType.CodeView) + { + CodeViewDebugDirectoryData codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); + using Stream pdbStream = _fileSystem.OpenRead(_sourceRootTranslator.ResolveFilePath(codeViewData.Path)); + using var metadataReaderProvider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); + MetadataReader metadataReader = null; + try + { + metadataReader = metadataReaderProvider.GetMetadataReader(); + } + catch (BadImageFormatException) + { + _logger.LogWarning($"{nameof(BadImageFormatException)} during MetadataReaderProvider.FromPortablePdbStream in InstrumentationHelper.PortablePdbHasLocalSource, unable to check if module has got local source."); return true; - } + } - private IEnumerable<(string documentName, bool documentExists)> DocumentSourceMap(MetadataReader metadataReader) - { - return metadataReader.Documents.Select(docHandle => - { - Document document = metadataReader.GetDocument(docHandle); - string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name)); - return (docName, _fileSystem.Exists(docName)); - }); + if (!MatchDocumentsWithSources(module, excludeAssembliesWithoutSources, metadataReader)) + { + return false; + } } + } - private bool MatchDocumentsWithSourcesMissingAll(MetadataReader metadataReader) - { - return DocumentSourceMap(metadataReader).Any(x => x.documentExists); - } + return true; + } - private (bool allDocumentsMatch, string notFoundDocument) MatchDocumentsWithSourcesMissingAny( - MetadataReader metadataReader) + private bool MatchDocumentsWithSources(string module, AssemblySearchType excludeAssembliesWithoutSources, + MetadataReader metadataReader) + { + if (excludeAssembliesWithoutSources.Equals(AssemblySearchType.MissingAll)) + { + bool anyDocumentMatches = MatchDocumentsWithSourcesMissingAll(metadataReader); + if (!anyDocumentMatches) { - var documentSourceMap = DocumentSourceMap(metadataReader).ToList(); + _logger.LogVerbose($"Excluding module from instrumentation: {module}, pdb without any local source files"); + return false; + } + } - if (documentSourceMap.Any(x => !x.documentExists)) - return (false, documentSourceMap.FirstOrDefault(x => !x.documentExists).documentName); + if (excludeAssembliesWithoutSources.Equals(AssemblySearchType.MissingAny)) + { + (bool allDocumentsMatch, string notFoundDocument) = MatchDocumentsWithSourcesMissingAny(metadataReader); - return (true, string.Empty); + if (!allDocumentsMatch) + { + _logger.LogVerbose( + $"Excluding module from instrumentation: {module}, pdb without local source files, [{FileSystem.EscapeFileName(notFoundDocument)}]"); + return false; } + } - public void BackupOriginalModule(string module, string identifier) - { - string backupPath = GetBackupPath(module, identifier); - string backupSymbolPath = Path.ChangeExtension(backupPath, ".pdb"); - _fileSystem.Copy(module, backupPath, true); - if (!_backupList.TryAdd(module, backupPath)) - { - throw new ArgumentException($"Key already added '{module}'"); - } + return true; + } - string symbolFile = Path.ChangeExtension(module, ".pdb"); - if (_fileSystem.Exists(symbolFile)) - { - _fileSystem.Copy(symbolFile, backupSymbolPath, true); - if (!_backupList.TryAdd(symbolFile, backupSymbolPath)) - { - throw new ArgumentException($"Key already added '{module}'"); - } - } - } + private IEnumerable<(string documentName, bool documentExists)> DocumentSourceMap(MetadataReader metadataReader) + { + return metadataReader.Documents.Select(docHandle => + { + Document document = metadataReader.GetDocument(docHandle); + string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name)); + return (docName, _fileSystem.Exists(docName)); + }); + } - public virtual void RestoreOriginalModule(string module, string identifier) - { - string backupPath = GetBackupPath(module, identifier); - string backupSymbolPath = Path.ChangeExtension(backupPath, ".pdb"); + private bool MatchDocumentsWithSourcesMissingAll(MetadataReader metadataReader) + { + return DocumentSourceMap(metadataReader).Any(x => x.documentExists); + } - // Restore the original module - retry up to 10 times, since the destination file could be locked - // See: https://github.com/tonerdo/coverlet/issues/25 - Func retryStrategy = CreateRetryStrategy(); + private (bool allDocumentsMatch, string notFoundDocument) MatchDocumentsWithSourcesMissingAny( + MetadataReader metadataReader) + { + var documentSourceMap = DocumentSourceMap(metadataReader).ToList(); - _retryHelper.Retry(() => - { - _fileSystem.Copy(backupPath, module, true); - _fileSystem.Delete(backupPath); - _backupList.TryRemove(module, out string _); - }, retryStrategy, RetryAttempts); + if (documentSourceMap.Any(x => !x.documentExists)) + return (false, documentSourceMap.FirstOrDefault(x => !x.documentExists).documentName); - _retryHelper.Retry(() => - { - if (_fileSystem.Exists(backupSymbolPath)) - { - string symbolFile = Path.ChangeExtension(module, ".pdb"); - _fileSystem.Copy(backupSymbolPath, symbolFile, true); - _fileSystem.Delete(backupSymbolPath); - _backupList.TryRemove(symbolFile, out string _); - } - }, retryStrategy, RetryAttempts); - } + return (true, string.Empty); + } - public virtual void RestoreOriginalModules() + public void BackupOriginalModule(string module, string identifier) + { + string backupPath = GetBackupPath(module, identifier); + string backupSymbolPath = Path.ChangeExtension(backupPath, ".pdb"); + _fileSystem.Copy(module, backupPath, true); + if (!_backupList.TryAdd(module, backupPath)) + { + throw new ArgumentException($"Key already added '{module}'"); + } + + string symbolFile = Path.ChangeExtension(module, ".pdb"); + if (_fileSystem.Exists(symbolFile)) + { + _fileSystem.Copy(symbolFile, backupSymbolPath, true); + if (!_backupList.TryAdd(symbolFile, backupSymbolPath)) { - // Restore the original module - retry up to 10 times, since the destination file could be locked - // See: https://github.com/tonerdo/coverlet/issues/25 - Func retryStrategy = CreateRetryStrategy(); - - foreach (string key in _backupList.Keys.ToList()) - { - string backupPath = _backupList[key]; - _retryHelper.Retry(() => - { - _fileSystem.Copy(backupPath, key, true); - _fileSystem.Delete(backupPath); - _backupList.TryRemove(key, out string _); - }, retryStrategy, RetryAttempts); - } + throw new ArgumentException($"Key already added '{module}'"); } + } + } - public void DeleteHitsFile(string path) + public virtual void RestoreOriginalModule(string module, string identifier) + { + string backupPath = GetBackupPath(module, identifier); + string backupSymbolPath = Path.ChangeExtension(backupPath, ".pdb"); + + // Restore the original module - retry up to 10 times, since the destination file could be locked + // See: https://github.com/tonerdo/coverlet/issues/25 + Func retryStrategy = CreateRetryStrategy(); + + _retryHelper.Retry(() => + { + _fileSystem.Copy(backupPath, module, true); + _fileSystem.Delete(backupPath); + _backupList.TryRemove(module, out string _); + }, retryStrategy, RetryAttempts); + + _retryHelper.Retry(() => + { + if (_fileSystem.Exists(backupSymbolPath)) { - Func retryStrategy = CreateRetryStrategy(); - _retryHelper.Retry(() => _fileSystem.Delete(path), retryStrategy, RetryAttempts); + string symbolFile = Path.ChangeExtension(module, ".pdb"); + _fileSystem.Copy(backupSymbolPath, symbolFile, true); + _fileSystem.Delete(backupSymbolPath); + _backupList.TryRemove(symbolFile, out string _); } + }, retryStrategy, RetryAttempts); + } - public bool IsValidFilterExpression(string filter) + public virtual void RestoreOriginalModules() + { + // Restore the original module - retry up to 10 times, since the destination file could be locked + // See: https://github.com/tonerdo/coverlet/issues/25 + Func retryStrategy = CreateRetryStrategy(); + + foreach (string key in _backupList.Keys.ToList()) + { + string backupPath = _backupList[key]; + _retryHelper.Retry(() => { - if (filter == null) - return false; - - if (!filter.StartsWith("[")) - return false; + _fileSystem.Copy(backupPath, key, true); + _fileSystem.Delete(backupPath); + _backupList.TryRemove(key, out string _); + }, retryStrategy, RetryAttempts); + } + } - if (!filter.Contains("]")) - return false; + public void DeleteHitsFile(string path) + { + Func retryStrategy = CreateRetryStrategy(); + _retryHelper.Retry(() => _fileSystem.Delete(path), retryStrategy, RetryAttempts); + } - if (filter.Count(f => f == '[') > 1) - return false; + public bool IsValidFilterExpression(string filter) + { + if (filter == null) + return false; - if (filter.Count(f => f == ']') > 1) - return false; + if (!filter.StartsWith("[")) + return false; - if (filter.IndexOf(']') < filter.IndexOf('[')) - return false; + if (!filter.Contains("]")) + return false; - if (filter.IndexOf(']') - filter.IndexOf('[') == 1) - return false; + if (filter.Count(f => f == '[') > 1) + return false; - if (filter.EndsWith("]")) - return false; + if (filter.Count(f => f == ']') > 1) + return false; - if (new Regex(@"[^\w*]").IsMatch(filter.Replace(".", "").Replace("?", "").Replace("[", "").Replace("]", ""))) - return false; + if (filter.IndexOf(']') < filter.IndexOf('[')) + return false; - return true; - } + if (filter.IndexOf(']') - filter.IndexOf('[') == 1) + return false; - public bool IsModuleExcluded(string module, string[] excludeFilters) - { - if (excludeFilters == null || excludeFilters.Length == 0) - return false; + if (filter.EndsWith("]")) + return false; - module = Path.GetFileNameWithoutExtension(module); - if (module == null) - return false; + if (new Regex(@"[^\w*]").IsMatch(filter.Replace(".", "").Replace("?", "").Replace("[", "").Replace("]", ""))) + return false; - foreach (string filter in excludeFilters) - { - string typePattern = filter.Substring(filter.IndexOf(']') + 1); + return true; + } - if (typePattern != "*") - continue; + public bool IsModuleExcluded(string module, string[] excludeFilters) + { + if (excludeFilters == null || excludeFilters.Length == 0) + return false; - string modulePattern = filter.Substring(1, filter.IndexOf(']') - 1); - modulePattern = WildcardToRegex(modulePattern); + module = Path.GetFileNameWithoutExtension(module); + if (module == null) + return false; - var regex = new Regex(modulePattern); + foreach (string filter in excludeFilters) + { + string typePattern = filter.Substring(filter.IndexOf(']') + 1); - if (regex.IsMatch(module)) - return true; - } + if (typePattern != "*") + continue; - return false; - } + string modulePattern = filter.Substring(1, filter.IndexOf(']') - 1); + modulePattern = WildcardToRegex(modulePattern); - public bool IsModuleIncluded(string module, string[] includeFilters) - { - if (includeFilters == null || includeFilters.Length == 0) - return true; + var regex = new Regex(modulePattern); - module = Path.GetFileNameWithoutExtension(module); - if (module == null) - return false; + if (regex.IsMatch(module)) + return true; + } - foreach (string filter in includeFilters) - { - string modulePattern = filter.Substring(1, filter.IndexOf(']') - 1); + return false; + } - if (modulePattern == "*") - return true; + public bool IsModuleIncluded(string module, string[] includeFilters) + { + if (includeFilters == null || includeFilters.Length == 0) + return true; - modulePattern = WildcardToRegex(modulePattern); + module = Path.GetFileNameWithoutExtension(module); + if (module == null) + return false; - var regex = new Regex(modulePattern); + foreach (string filter in includeFilters) + { + string modulePattern = filter.Substring(1, filter.IndexOf(']') - 1); - if (regex.IsMatch(module)) - return true; - } + if (modulePattern == "*") + return true; - return false; - } + modulePattern = WildcardToRegex(modulePattern); - public bool IsTypeExcluded(string module, string type, string[] excludeFilters) - { - if (excludeFilters == null || excludeFilters.Length == 0) - return false; + var regex = new Regex(modulePattern); - module = Path.GetFileNameWithoutExtension(module); - if (module == null) - return false; + if (regex.IsMatch(module)) + return true; + } - return IsTypeFilterMatch(module, type, excludeFilters); - } + return false; + } - public bool IsTypeIncluded(string module, string type, string[] includeFilters) - { - if (includeFilters == null || includeFilters.Length == 0) - return true; + public bool IsTypeExcluded(string module, string type, string[] excludeFilters) + { + if (excludeFilters == null || excludeFilters.Length == 0) + return false; - module = Path.GetFileNameWithoutExtension(module); - if (module == null) - return true; + module = Path.GetFileNameWithoutExtension(module); + if (module == null) + return false; - return IsTypeFilterMatch(module, type, includeFilters); - } + return IsTypeFilterMatch(module, type, excludeFilters); + } - public bool IsLocalMethod(string method) - => new Regex(WildcardToRegex("<*>*__*|*")).IsMatch(method); + public bool IsTypeIncluded(string module, string type, string[] includeFilters) + { + if (includeFilters == null || includeFilters.Length == 0) + return true; - public void SetLogger(ILogger logger) - { - _logger = logger; - } + module = Path.GetFileNameWithoutExtension(module); + if (module == null) + return true; - private static bool IsTypeFilterMatch(string module, string type, string[] filters) - { - Debug.Assert(module != null); - Debug.Assert(filters != null); + return IsTypeFilterMatch(module, type, includeFilters); + } - foreach (string filter in filters) - { - string typePattern = filter.Substring(filter.IndexOf(']') + 1); - string modulePattern = filter.Substring(1, filter.IndexOf(']') - 1); + public bool IsLocalMethod(string method) + => new Regex(WildcardToRegex("<*>*__*|*")).IsMatch(method); - typePattern = WildcardToRegex(typePattern); - modulePattern = WildcardToRegex(modulePattern); + public void SetLogger(ILogger logger) + { + _logger = logger; + } - if (new Regex(typePattern).IsMatch(type) && new Regex(modulePattern).IsMatch(module)) - return true; - } + private static bool IsTypeFilterMatch(string module, string type, string[] filters) + { + Debug.Assert(module != null); + Debug.Assert(filters != null); - return false; - } + foreach (string filter in filters) + { + string typePattern = filter.Substring(filter.IndexOf(']') + 1); + string modulePattern = filter.Substring(1, filter.IndexOf(']') - 1); - private static string GetBackupPath(string module, string identifier) - { - return Path.Combine( - Path.GetTempPath(), - Path.GetFileNameWithoutExtension(module) + "_" + identifier + ".dll" - ); - } + typePattern = WildcardToRegex(typePattern); + modulePattern = WildcardToRegex(modulePattern); - private Func CreateRetryStrategy(int initialSleepSeconds = 6) - { - TimeSpan retryStrategy() - { - var sleep = TimeSpan.FromMilliseconds(initialSleepSeconds); - initialSleepSeconds *= 2; - return sleep; - } + if (new Regex(typePattern).IsMatch(type) && new Regex(modulePattern).IsMatch(module)) + return true; + } - return retryStrategy; - } + return false; + } - private static string WildcardToRegex(string pattern) - { - return "^" + Regex.Escape(pattern). - Replace("\\*", ".*"). - Replace("\\?", "?") + "$"; - } + private static string GetBackupPath(string module, string identifier) + { + return Path.Combine( + Path.GetTempPath(), + Path.GetFileNameWithoutExtension(module) + "_" + identifier + ".dll" + ); + } - private static bool IsAssembly(string filePath) - { - Debug.Assert(filePath != null); + private Func CreateRetryStrategy(int initialSleepSeconds = 6) + { + TimeSpan retryStrategy() + { + var sleep = TimeSpan.FromMilliseconds(initialSleepSeconds); + initialSleepSeconds *= 2; + return sleep; + } + + return retryStrategy; + } - if (!(filePath.EndsWith(".exe") || filePath.EndsWith(".dll"))) - return false; + private static string WildcardToRegex(string pattern) + { + return "^" + Regex.Escape(pattern). + Replace("\\*", ".*"). + Replace("\\?", "?") + "$"; + } - try - { - AssemblyName.GetAssemblyName(filePath); - return true; - } - catch - { - return false; - } - } + private static bool IsAssembly(string filePath) + { + Debug.Assert(filePath != null); + + if (!(filePath.EndsWith(".exe") || filePath.EndsWith(".dll"))) + return false; + + try + { + AssemblyName.GetAssemblyName(filePath); + return true; + } + catch + { + return false; + } } + } } diff --git a/src/coverlet.core/Helpers/ProcessExitHandler.cs b/src/coverlet.core/Helpers/ProcessExitHandler.cs index 1e570f07f..e941405d3 100644 --- a/src/coverlet.core/Helpers/ProcessExitHandler.cs +++ b/src/coverlet.core/Helpers/ProcessExitHandler.cs @@ -6,11 +6,11 @@ namespace Coverlet.Core.Helpers { - internal class ProcessExitHandler : IProcessExitHandler + internal class ProcessExitHandler : IProcessExitHandler + { + public void Add(EventHandler handler) { - public void Add(EventHandler handler) - { - AppDomain.CurrentDomain.ProcessExit += handler; - } + AppDomain.CurrentDomain.ProcessExit += handler; } + } } diff --git a/src/coverlet.core/Helpers/RetryHelper.cs b/src/coverlet.core/Helpers/RetryHelper.cs index dcf3fa9d0..f28361dac 100644 --- a/src/coverlet.core/Helpers/RetryHelper.cs +++ b/src/coverlet.core/Helpers/RetryHelper.cs @@ -8,58 +8,58 @@ namespace Coverlet.Core.Helpers { - // A slightly amended version of the code found here: https://stackoverflow.com/a/1563234/186184 - // This code allows for varying backoff strategies through the use of Func. - internal class RetryHelper : IRetryHelper + // A slightly amended version of the code found here: https://stackoverflow.com/a/1563234/186184 + // This code allows for varying backoff strategies through the use of Func. + internal class RetryHelper : IRetryHelper + { + /// + /// Retry a void method. + /// + /// The action to perform + /// A function returning a Timespan defining the backoff strategy to use. + /// The maximum number of retries before bailing out. Defaults to 3. + public void Retry( + Action action, + Func backoffStrategy, + int maxAttemptCount = 3) { - /// - /// Retry a void method. - /// - /// The action to perform - /// A function returning a Timespan defining the backoff strategy to use. - /// The maximum number of retries before bailing out. Defaults to 3. - public void Retry( - Action action, - Func backoffStrategy, - int maxAttemptCount = 3) + Do(() => + { + action(); + return null; + }, backoffStrategy, maxAttemptCount); + } + + /// + /// Retry a method returning type T. + /// + /// The type of the object to return + /// The action to perform + /// A function returning a Timespan defining the backoff strategy to use. + /// The maximum number of retries before bailing out. Defaults to 3. + public T Do( + Func action, + Func backoffStrategy, + int maxAttemptCount = 3) + { + var exceptions = new List(); + + for (int attempted = 0; attempted < maxAttemptCount; attempted++) + { + try { - Do(() => - { - action(); - return null; - }, backoffStrategy, maxAttemptCount); + if (attempted > 0) + { + Thread.Sleep(backoffStrategy()); + } + return action(); } - - /// - /// Retry a method returning type T. - /// - /// The type of the object to return - /// The action to perform - /// A function returning a Timespan defining the backoff strategy to use. - /// The maximum number of retries before bailing out. Defaults to 3. - public T Do( - Func action, - Func backoffStrategy, - int maxAttemptCount = 3) + catch (Exception ex) { - var exceptions = new List(); - - for (int attempted = 0; attempted < maxAttemptCount; attempted++) - { - try - { - if (attempted > 0) - { - Thread.Sleep(backoffStrategy()); - } - return action(); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - } - throw new AggregateException(exceptions); + exceptions.Add(ex); } + } + throw new AggregateException(exceptions); } + } } diff --git a/src/coverlet.core/Helpers/SourceRootTranslator.cs b/src/coverlet.core/Helpers/SourceRootTranslator.cs index 7fea89516..89d03fdb3 100644 --- a/src/coverlet.core/Helpers/SourceRootTranslator.cs +++ b/src/coverlet.core/Helpers/SourceRootTranslator.cs @@ -9,165 +9,165 @@ namespace Coverlet.Core.Helpers { - [DebuggerDisplay("ProjectPath = {ProjectPath} OriginalPath = {OriginalPath}")] - internal class SourceRootMapping + [DebuggerDisplay("ProjectPath = {ProjectPath} OriginalPath = {OriginalPath}")] + internal class SourceRootMapping + { + public string ProjectPath { get; set; } + public string OriginalPath { get; set; } + } + + internal class SourceRootTranslator : ISourceRootTranslator + { + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly Dictionary> _sourceRootMapping; + private readonly Dictionary> _sourceToDeterministicPathMapping; + private readonly string _mappingFileName; + private Dictionary _resolutionCacheFiles; + + public SourceRootTranslator(ILogger logger, IFileSystem fileSystem) { - public string ProjectPath { get; set; } - public string OriginalPath { get; set; } + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _sourceRootMapping = new Dictionary>(); } - internal class SourceRootTranslator : ISourceRootTranslator + public SourceRootTranslator(string moduleTestPath, ILogger logger, IFileSystem fileSystem, IAssemblyAdapter assemblyAdapter) { - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly Dictionary> _sourceRootMapping; - private readonly Dictionary> _sourceToDeterministicPathMapping; - private readonly string _mappingFileName; - private Dictionary _resolutionCacheFiles; - - public SourceRootTranslator(ILogger logger, IFileSystem fileSystem) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); - _sourceRootMapping = new Dictionary>(); - } + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + if (moduleTestPath is null) + { + throw new ArgumentNullException(nameof(moduleTestPath)); + } + if (!_fileSystem.Exists(moduleTestPath)) + { + throw new FileNotFoundException($"Module test path '{moduleTestPath}' not found", moduleTestPath); + } + + string assemblyName = assemblyAdapter.GetAssemblyName(moduleTestPath); + _mappingFileName = $"CoverletSourceRootsMapping_{assemblyName}"; + + _sourceRootMapping = LoadSourceRootMapping(Path.GetDirectoryName(moduleTestPath)); + _sourceToDeterministicPathMapping = LoadSourceToDeterministicPathMapping(_sourceRootMapping); + } - public SourceRootTranslator(string moduleTestPath, ILogger logger, IFileSystem fileSystem, IAssemblyAdapter assemblyAdapter) + private static Dictionary> LoadSourceToDeterministicPathMapping(Dictionary> sourceRootMapping) + { + if (sourceRootMapping is null) + { + throw new ArgumentNullException(nameof(sourceRootMapping)); + } + + var sourceToDeterministicPathMapping = new Dictionary>(); + foreach (KeyValuePair> sourceRootMappingEntry in sourceRootMapping) + { + foreach (SourceRootMapping originalPath in sourceRootMappingEntry.Value) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); - if (moduleTestPath is null) - { - throw new ArgumentNullException(nameof(moduleTestPath)); - } - if (!_fileSystem.Exists(moduleTestPath)) - { - throw new FileNotFoundException($"Module test path '{moduleTestPath}' not found", moduleTestPath); - } + if (!sourceToDeterministicPathMapping.ContainsKey(originalPath.OriginalPath)) + { + sourceToDeterministicPathMapping.Add(originalPath.OriginalPath, new List()); + } + sourceToDeterministicPathMapping[originalPath.OriginalPath].Add(sourceRootMappingEntry.Key); + } + } - string assemblyName = assemblyAdapter.GetAssemblyName(moduleTestPath); - _mappingFileName = $"CoverletSourceRootsMapping_{assemblyName}"; + return sourceToDeterministicPathMapping; + } - _sourceRootMapping = LoadSourceRootMapping(Path.GetDirectoryName(moduleTestPath)); - _sourceToDeterministicPathMapping = LoadSourceToDeterministicPathMapping(_sourceRootMapping); + private Dictionary> LoadSourceRootMapping(string directory) + { + var mapping = new Dictionary>(); + + string mappingFilePath = Path.Combine(directory, _mappingFileName); + if (!_fileSystem.Exists(mappingFilePath)) + { + return mapping; + } + + foreach (string mappingRecord in _fileSystem.ReadAllLines(mappingFilePath)) + { + int projectFileSeparatorIndex = mappingRecord.IndexOf('|'); + int pathMappingSeparatorIndex = mappingRecord.IndexOf('='); + if (projectFileSeparatorIndex == -1 || pathMappingSeparatorIndex == -1) + { + _logger.LogWarning($"Malformed mapping '{mappingRecord}'"); + continue; } + string projectPath = mappingRecord.Substring(0, projectFileSeparatorIndex); + string originalPath = mappingRecord.Substring(projectFileSeparatorIndex + 1, pathMappingSeparatorIndex - projectFileSeparatorIndex - 1); + string mappedPath = mappingRecord.Substring(pathMappingSeparatorIndex + 1); - private static Dictionary> LoadSourceToDeterministicPathMapping(Dictionary> sourceRootMapping) + if (!mapping.ContainsKey(mappedPath)) { - if (sourceRootMapping is null) - { - throw new ArgumentNullException(nameof(sourceRootMapping)); - } - - var sourceToDeterministicPathMapping = new Dictionary>(); - foreach (KeyValuePair> sourceRootMappingEntry in sourceRootMapping) - { - foreach (SourceRootMapping originalPath in sourceRootMappingEntry.Value) - { - if (!sourceToDeterministicPathMapping.ContainsKey(originalPath.OriginalPath)) - { - sourceToDeterministicPathMapping.Add(originalPath.OriginalPath, new List()); - } - sourceToDeterministicPathMapping[originalPath.OriginalPath].Add(sourceRootMappingEntry.Key); - } - } - - return sourceToDeterministicPathMapping; + mapping.Add(mappedPath, new List()); } - private Dictionary> LoadSourceRootMapping(string directory) + foreach (string path in originalPath.Split(';')) { - var mapping = new Dictionary>(); - - string mappingFilePath = Path.Combine(directory, _mappingFileName); - if (!_fileSystem.Exists(mappingFilePath)) - { - return mapping; - } - - foreach (string mappingRecord in _fileSystem.ReadAllLines(mappingFilePath)) - { - int projectFileSeparatorIndex = mappingRecord.IndexOf('|'); - int pathMappingSeparatorIndex = mappingRecord.IndexOf('='); - if (projectFileSeparatorIndex == -1 || pathMappingSeparatorIndex == -1) - { - _logger.LogWarning($"Malformed mapping '{mappingRecord}'"); - continue; - } - string projectPath = mappingRecord.Substring(0, projectFileSeparatorIndex); - string originalPath = mappingRecord.Substring(projectFileSeparatorIndex + 1, pathMappingSeparatorIndex - projectFileSeparatorIndex - 1); - string mappedPath = mappingRecord.Substring(pathMappingSeparatorIndex + 1); - - if (!mapping.ContainsKey(mappedPath)) - { - mapping.Add(mappedPath, new List()); - } - - foreach (string path in originalPath.Split(';')) - { - mapping[mappedPath].Add(new SourceRootMapping() { OriginalPath = path, ProjectPath = projectPath }); - } - } - - return mapping; + mapping[mappedPath].Add(new SourceRootMapping() { OriginalPath = path, ProjectPath = projectPath }); } + } - public bool AddMappingInCache(string originalFileName, string targetFileName) - { - if (_resolutionCacheFiles != null && _resolutionCacheFiles.ContainsKey(originalFileName)) - { - return false; - } + return mapping; + } - (_resolutionCacheFiles ??= new Dictionary()).Add(originalFileName, targetFileName); - return true; - } + public bool AddMappingInCache(string originalFileName, string targetFileName) + { + if (_resolutionCacheFiles != null && _resolutionCacheFiles.ContainsKey(originalFileName)) + { + return false; + } - public IReadOnlyList ResolvePathRoot(string pathRoot) - { - return _sourceRootMapping.TryGetValue(pathRoot, out List sourceRootMapping) ? sourceRootMapping.AsReadOnly() : null; - } + (_resolutionCacheFiles ??= new Dictionary()).Add(originalFileName, targetFileName); + return true; + } - public string ResolveFilePath(string originalFileName) - { - if (_resolutionCacheFiles != null && _resolutionCacheFiles.ContainsKey(originalFileName)) - { - return _resolutionCacheFiles[originalFileName]; - } + public IReadOnlyList ResolvePathRoot(string pathRoot) + { + return _sourceRootMapping.TryGetValue(pathRoot, out List sourceRootMapping) ? sourceRootMapping.AsReadOnly() : null; + } - foreach (KeyValuePair> mapping in _sourceRootMapping) + public string ResolveFilePath(string originalFileName) + { + if (_resolutionCacheFiles != null && _resolutionCacheFiles.ContainsKey(originalFileName)) + { + return _resolutionCacheFiles[originalFileName]; + } + + foreach (KeyValuePair> mapping in _sourceRootMapping) + { + if (originalFileName.StartsWith(mapping.Key)) + { + foreach (SourceRootMapping srm in mapping.Value) + { + string pathToCheck; + if (_fileSystem.Exists(pathToCheck = Path.GetFullPath(originalFileName.Replace(mapping.Key, srm.OriginalPath)))) { - if (originalFileName.StartsWith(mapping.Key)) - { - foreach (SourceRootMapping srm in mapping.Value) - { - string pathToCheck; - if (_fileSystem.Exists(pathToCheck = Path.GetFullPath(originalFileName.Replace(mapping.Key, srm.OriginalPath)))) - { - (_resolutionCacheFiles ??= new Dictionary()).Add(originalFileName, pathToCheck); - _logger.LogVerbose($"Mapping resolved: '{FileSystem.EscapeFileName(originalFileName)}' -> '{FileSystem.EscapeFileName(pathToCheck)}'"); - return pathToCheck; - } - } - } + (_resolutionCacheFiles ??= new Dictionary()).Add(originalFileName, pathToCheck); + _logger.LogVerbose($"Mapping resolved: '{FileSystem.EscapeFileName(originalFileName)}' -> '{FileSystem.EscapeFileName(pathToCheck)}'"); + return pathToCheck; } - return originalFileName; + } } + } + return originalFileName; + } - public string ResolveDeterministicPath(string originalFileName) + public string ResolveDeterministicPath(string originalFileName) + { + foreach (KeyValuePair> originalPath in _sourceToDeterministicPathMapping) + { + if (originalFileName.StartsWith(originalPath.Key)) { - foreach (KeyValuePair> originalPath in _sourceToDeterministicPathMapping) - { - if (originalFileName.StartsWith(originalPath.Key)) - { - foreach (string deterministicPath in originalPath.Value) - { - originalFileName = originalFileName.Replace(originalPath.Key, deterministicPath).Replace('\\', '/'); - } - } - } - - return originalFileName; + foreach (string deterministicPath in originalPath.Value) + { + originalFileName = originalFileName.Replace(originalPath.Key, deterministicPath).Replace('\\', '/'); + } } + } + + return originalFileName; } + } } diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index df43496c9..606eb40e7 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -21,946 +21,946 @@ namespace Coverlet.Core.Instrumentation { - internal class Instrumenter + internal class Instrumenter + { + private readonly string _module; + private readonly string _identifier; + private readonly ExcludedFilesHelper _excludedFilesHelper; + private readonly CoverageParameters _parameters; + private readonly string[] _excludedAttributes; + private readonly bool _isCoreLibrary; + private readonly ILogger _logger; + private readonly IInstrumentationHelper _instrumentationHelper; + private readonly IFileSystem _fileSystem; + private readonly ISourceRootTranslator _sourceRootTranslator; + private readonly ICecilSymbolHelper _cecilSymbolHelper; + private readonly string[] _doesNotReturnAttributes; + private readonly AssemblySearchType _excludeAssembliesWithoutSources; + private InstrumenterResult _result; + private FieldDefinition _customTrackerHitsArray; + private FieldDefinition _customTrackerHitsFilePath; + private FieldDefinition _customTrackerSingleHit; + private FieldDefinition _customTrackerFlushHitFile; + private ILProcessor _customTrackerClassConstructorIl; + private TypeDefinition _customTrackerTypeDef; + private MethodReference _customTrackerRegisterUnloadEventsMethod; + private MethodReference _customTrackerRecordHitMethod; + private List _excludedSourceFiles; + private List _branchesInCompiledGeneratedClass; + private List<(MethodDefinition, int)> _excludedMethods; + private List _excludedLambdaMethods; + private List _excludedCompilerGeneratedTypes; + private ReachabilityHelper _reachabilityHelper; + + public bool SkipModule { get; set; } + + public Instrumenter( + string module, + string identifier, + CoverageParameters parameters, + ILogger logger, + IInstrumentationHelper instrumentationHelper, + IFileSystem fileSystem, + ISourceRootTranslator sourceRootTranslator, + ICecilSymbolHelper cecilSymbolHelper) { - private readonly string _module; - private readonly string _identifier; - private readonly ExcludedFilesHelper _excludedFilesHelper; - private readonly CoverageParameters _parameters; - private readonly string[] _excludedAttributes; - private readonly bool _isCoreLibrary; - private readonly ILogger _logger; - private readonly IInstrumentationHelper _instrumentationHelper; - private readonly IFileSystem _fileSystem; - private readonly ISourceRootTranslator _sourceRootTranslator; - private readonly ICecilSymbolHelper _cecilSymbolHelper; - private readonly string[] _doesNotReturnAttributes; - private readonly AssemblySearchType _excludeAssembliesWithoutSources; - private InstrumenterResult _result; - private FieldDefinition _customTrackerHitsArray; - private FieldDefinition _customTrackerHitsFilePath; - private FieldDefinition _customTrackerSingleHit; - private FieldDefinition _customTrackerFlushHitFile; - private ILProcessor _customTrackerClassConstructorIl; - private TypeDefinition _customTrackerTypeDef; - private MethodReference _customTrackerRegisterUnloadEventsMethod; - private MethodReference _customTrackerRecordHitMethod; - private List _excludedSourceFiles; - private List _branchesInCompiledGeneratedClass; - private List<(MethodDefinition, int)> _excludedMethods; - private List _excludedLambdaMethods; - private List _excludedCompilerGeneratedTypes; - private ReachabilityHelper _reachabilityHelper; - - public bool SkipModule { get; set; } - - public Instrumenter( - string module, - string identifier, - CoverageParameters parameters, - ILogger logger, - IInstrumentationHelper instrumentationHelper, - IFileSystem fileSystem, - ISourceRootTranslator sourceRootTranslator, - ICecilSymbolHelper cecilSymbolHelper) - { - _module = module; - _identifier = identifier; - _parameters = parameters; - _excludedFilesHelper = new ExcludedFilesHelper(parameters.ExcludedSourceFiles, logger); - _excludedAttributes = PrepareAttributes(parameters.ExcludeAttributes, nameof(ExcludeFromCoverageAttribute), nameof(ExcludeFromCodeCoverageAttribute)); - _isCoreLibrary = Path.GetFileNameWithoutExtension(_module) == "System.Private.CoreLib"; - _logger = logger; - _instrumentationHelper = instrumentationHelper; - _fileSystem = fileSystem; - _sourceRootTranslator = sourceRootTranslator; - _cecilSymbolHelper = cecilSymbolHelper; - _doesNotReturnAttributes = PrepareAttributes(parameters.DoesNotReturnAttributes); - _excludeAssembliesWithoutSources = DetermineHeuristics(parameters.ExcludeAssembliesWithoutSources); - } - - private AssemblySearchType DetermineHeuristics(string parametersExcludeAssembliesWithoutSources) - { - if (Enum.TryParse(parametersExcludeAssembliesWithoutSources, true, out AssemblySearchType option)) - { - return option; - } - return AssemblySearchType.MissingAll; - } + _module = module; + _identifier = identifier; + _parameters = parameters; + _excludedFilesHelper = new ExcludedFilesHelper(parameters.ExcludedSourceFiles, logger); + _excludedAttributes = PrepareAttributes(parameters.ExcludeAttributes, nameof(ExcludeFromCoverageAttribute), nameof(ExcludeFromCodeCoverageAttribute)); + _isCoreLibrary = Path.GetFileNameWithoutExtension(_module) == "System.Private.CoreLib"; + _logger = logger; + _instrumentationHelper = instrumentationHelper; + _fileSystem = fileSystem; + _sourceRootTranslator = sourceRootTranslator; + _cecilSymbolHelper = cecilSymbolHelper; + _doesNotReturnAttributes = PrepareAttributes(parameters.DoesNotReturnAttributes); + _excludeAssembliesWithoutSources = DetermineHeuristics(parameters.ExcludeAssembliesWithoutSources); + } + + private AssemblySearchType DetermineHeuristics(string parametersExcludeAssembliesWithoutSources) + { + if (Enum.TryParse(parametersExcludeAssembliesWithoutSources, true, out AssemblySearchType option)) + { + return option; + } + return AssemblySearchType.MissingAll; + } + + private static string[] PrepareAttributes(IEnumerable providedAttrs, params string[] defaultAttrs) + { + return + (providedAttrs ?? Array.Empty()) + // In case the attribute class ends in "Attribute", but it wasn't specified. + // Both names are included (if it wasn't specified) because the attribute class might not actually end in the prefix. + .SelectMany(a => a.EndsWith("Attribute") ? new[] { a } : new[] { a, $"{a}Attribute" }) + // The default custom attributes used to exclude from coverage. + .Union(defaultAttrs) + .ToArray(); + } - private static string[] PrepareAttributes(IEnumerable providedAttrs, params string[] defaultAttrs) + public bool CanInstrument() + { + try + { + if (_instrumentationHelper.HasPdb(_module, out bool embeddedPdb)) { - return - (providedAttrs ?? Array.Empty()) - // In case the attribute class ends in "Attribute", but it wasn't specified. - // Both names are included (if it wasn't specified) because the attribute class might not actually end in the prefix. - .SelectMany(a => a.EndsWith("Attribute") ? new[] { a } : new[] { a, $"{a}Attribute" }) - // The default custom attributes used to exclude from coverage. - .Union(defaultAttrs) - .ToArray(); + if (_excludeAssembliesWithoutSources.Equals(AssemblySearchType.None)) + { + return true; + } + + if (embeddedPdb) + { + return _instrumentationHelper.EmbeddedPortablePdbHasLocalSource(_module, _excludeAssembliesWithoutSources); + } + else + { + return _instrumentationHelper.PortablePdbHasLocalSource(_module, _excludeAssembliesWithoutSources); + } } - - public bool CanInstrument() + else { - try - { - if (_instrumentationHelper.HasPdb(_module, out bool embeddedPdb)) - { - if (_excludeAssembliesWithoutSources.Equals(AssemblySearchType.None)) - { - return true; - } - - if (embeddedPdb) - { - return _instrumentationHelper.EmbeddedPortablePdbHasLocalSource(_module, _excludeAssembliesWithoutSources); - } - else - { - return _instrumentationHelper.PortablePdbHasLocalSource(_module, _excludeAssembliesWithoutSources); - } - } - else - { - return false; - } - } - catch (Exception ex) - { - _logger.LogWarning($"Unable to instrument module: '{_module}'\n{ex}"); - return false; - } + return false; } + } + catch (Exception ex) + { + _logger.LogWarning($"Unable to instrument module: '{_module}'\n{ex}"); + return false; + } + } - public InstrumenterResult Instrument() + public InstrumenterResult Instrument() + { + string hitsFilePath = Path.Combine( + Path.GetTempPath(), + Path.GetFileNameWithoutExtension(_module) + "_" + _identifier + ); + + _result = new InstrumenterResult + { + Module = Path.GetFileNameWithoutExtension(_module), + HitsFilePath = hitsFilePath, + ModulePath = _module + }; + + InstrumentModule(); + + if (_excludedSourceFiles != null) + { + foreach (string sourceFile in _excludedSourceFiles) { - string hitsFilePath = Path.Combine( - Path.GetTempPath(), - Path.GetFileNameWithoutExtension(_module) + "_" + _identifier - ); + _logger.LogVerbose($"Excluded source file: '{FileSystem.EscapeFileName(sourceFile)}'"); + } + } - _result = new InstrumenterResult - { - Module = Path.GetFileNameWithoutExtension(_module), - HitsFilePath = hitsFilePath, - ModulePath = _module - }; + _result.BranchesInCompiledGeneratedClass = _branchesInCompiledGeneratedClass == null ? Array.Empty() : _branchesInCompiledGeneratedClass.ToArray(); - InstrumentModule(); + return _result; + } - if (_excludedSourceFiles != null) - { - foreach (string sourceFile in _excludedSourceFiles) - { - _logger.LogVerbose($"Excluded source file: '{FileSystem.EscapeFileName(sourceFile)}'"); - } - } + // If current type or one of his parent is excluded we'll exclude it + // If I'm out every my children and every children of my children will be out + private bool IsTypeExcluded(TypeDefinition type) + { + for (TypeDefinition current = type; current != null; current = current.DeclaringType) + { + // Check exclude attribute and filters + if (current.CustomAttributes.Any(IsExcludeAttribute) || _instrumentationHelper.IsTypeExcluded(_module, current.FullName, _parameters.ExcludeFilters)) + { + return true; + } + } - _result.BranchesInCompiledGeneratedClass = _branchesInCompiledGeneratedClass == null ? Array.Empty() : _branchesInCompiledGeneratedClass.ToArray(); + return false; + } - return _result; - } + // Instrumenting Interlocked which is used for recording hits would cause an infinite loop. + private bool Is_System_Threading_Interlocked_CoreLib_Type(TypeDefinition type) + { + return _isCoreLibrary && type.FullName == "System.Threading.Interlocked"; + } - // If current type or one of his parent is excluded we'll exclude it - // If I'm out every my children and every children of my children will be out - private bool IsTypeExcluded(TypeDefinition type) - { - for (TypeDefinition current = type; current != null; current = current.DeclaringType) - { - // Check exclude attribute and filters - if (current.CustomAttributes.Any(IsExcludeAttribute) || _instrumentationHelper.IsTypeExcluded(_module, current.FullName, _parameters.ExcludeFilters)) - { - return true; - } - } + // Have to do this before we start writing to a module, as we'll get into file + // locking issues if we do it while writing. + private void CreateReachabilityHelper() + { + using Stream stream = _fileSystem.NewFileStream(_module, FileMode.Open, FileAccess.Read); + using var resolver = new NetstandardAwareAssemblyResolver(_module, _logger); + resolver.AddSearchDirectory(Path.GetDirectoryName(_module)); + var parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = resolver }; + if (_isCoreLibrary) + { + parameters.MetadataImporterProvider = new CoreLibMetadataImporterProvider(); + } + + using var module = ModuleDefinition.ReadModule(stream, parameters); + _reachabilityHelper = ReachabilityHelper.CreateForModule(module, _doesNotReturnAttributes, _logger); + } - return false; + private void InstrumentModule() + { + CreateReachabilityHelper(); + + using Stream stream = _fileSystem.NewFileStream(_module, FileMode.Open, FileAccess.ReadWrite); + using var resolver = new NetstandardAwareAssemblyResolver(_module, _logger); + resolver.AddSearchDirectory(Path.GetDirectoryName(_module)); + var parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = resolver }; + if (_isCoreLibrary) + { + parameters.MetadataImporterProvider = new CoreLibMetadataImporterProvider(); + } + + using var module = ModuleDefinition.ReadModule(stream, parameters); + foreach (CustomAttribute customAttribute in module.Assembly.CustomAttributes) + { + if (IsExcludeAttribute(customAttribute)) + { + _logger.LogVerbose($"Excluded module: '{module}' for assembly level attribute {customAttribute.AttributeType.FullName}"); + SkipModule = true; + return; } - - // Instrumenting Interlocked which is used for recording hits would cause an infinite loop. - private bool Is_System_Threading_Interlocked_CoreLib_Type(TypeDefinition type) + } + + bool containsAppContext = module.GetType(nameof(System), nameof(AppContext)) != null; + IEnumerable types = module.GetTypes(); + AddCustomModuleTrackerToModule(module); + + CustomDebugInformation sourceLinkDebugInfo = module.CustomDebugInformations.FirstOrDefault(c => c.Kind == CustomDebugInformationKind.SourceLink); + if (sourceLinkDebugInfo != null) + { + _result.SourceLink = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content; + } + + foreach (TypeDefinition type in types) + { + if ( + !Is_System_Threading_Interlocked_CoreLib_Type(type) && + !IsTypeExcluded(type) && + _instrumentationHelper.IsTypeIncluded(_module, type.FullName, _parameters.IncludeFilters) + ) { - return _isCoreLibrary && type.FullName == "System.Threading.Interlocked"; + if (IsSynthesizedMemberToBeExcluded(type)) + { + (_excludedCompilerGeneratedTypes ??= new List()).Add(type.FullName); + } + else + { + InstrumentType(type); + } } - - // Have to do this before we start writing to a module, as we'll get into file - // locking issues if we do it while writing. - private void CreateReachabilityHelper() + } + + // Fixup the custom tracker class constructor, according to all instrumented types + if (_customTrackerRegisterUnloadEventsMethod == null) + { + _customTrackerRegisterUnloadEventsMethod = new MethodReference( + nameof(ModuleTrackerTemplate.RegisterUnloadEvents), module.TypeSystem.Void, _customTrackerTypeDef); + } + + Instruction lastInstr = _customTrackerClassConstructorIl.Body.Instructions.Last(); + + if (!containsAppContext) + { + // For "normal" cases, where the instrumented assembly is not the core library, we add a call to + // RegisterUnloadEvents to the static constructor of the generated custom tracker. Due to static + // initialization constraints, the core library is handled separately below. + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Call, _customTrackerRegisterUnloadEventsMethod)); + } + + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldc_I4, _result.HitCandidates.Count)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Newarr, module.TypeSystem.Int32)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsArray)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsFilePath)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(_parameters.SingleHit ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerSingleHit)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldc_I4_1)); + _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerFlushHitFile)); + + if (containsAppContext) + { + // Handle the core library by instrumenting System.AppContext.OnProcessExit to directly call + // the UnloadModule method of the custom tracker type. This avoids loops between the static + // initialization of the custom tracker and the static initialization of the hosting AppDomain + // (which for the core library case will be instrumented code). + var eventArgsType = new TypeReference(nameof(System), nameof(EventArgs), module, module.TypeSystem.CoreLibrary); + var customTrackerUnloadModule = new MethodReference(nameof(ModuleTrackerTemplate.UnloadModule), module.TypeSystem.Void, _customTrackerTypeDef); + customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object)); + customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(eventArgsType)); + + var appContextType = new TypeReference(nameof(System), nameof(AppContext), module, module.TypeSystem.CoreLibrary); + MethodDefinition onProcessExitMethod = new MethodReference("OnProcessExit", module.TypeSystem.Void, appContextType).Resolve(); + ILProcessor onProcessExitIl = onProcessExitMethod.Body.GetILProcessor(); + + // Put the OnProcessExit body inside try/finally to ensure the call to the UnloadModule. + Instruction lastInst = onProcessExitMethod.Body.Instructions.Last(); + var firstNullParam = Instruction.Create(OpCodes.Ldnull); + var secondNullParam = Instruction.Create(OpCodes.Ldnull); + var callUnload = Instruction.Create(OpCodes.Call, customTrackerUnloadModule); + onProcessExitIl.InsertAfter(lastInst, firstNullParam); + onProcessExitIl.InsertAfter(firstNullParam, secondNullParam); + onProcessExitIl.InsertAfter(secondNullParam, callUnload); + var endFinally = Instruction.Create(OpCodes.Endfinally); + onProcessExitIl.InsertAfter(callUnload, endFinally); + Instruction ret = onProcessExitIl.Create(OpCodes.Ret); + Instruction leaveAfterFinally = onProcessExitIl.Create(OpCodes.Leave, ret); + onProcessExitIl.InsertAfter(endFinally, ret); + foreach (Instruction inst in onProcessExitMethod.Body.Instructions.ToArray()) { - using Stream stream = _fileSystem.NewFileStream(_module, FileMode.Open, FileAccess.Read); - using var resolver = new NetstandardAwareAssemblyResolver(_module, _logger); - resolver.AddSearchDirectory(Path.GetDirectoryName(_module)); - var parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = resolver }; - if (_isCoreLibrary) - { - parameters.MetadataImporterProvider = new CoreLibMetadataImporterProvider(); - } + // Patch ret to leave after the finally + if (inst.OpCode == OpCodes.Ret && inst != ret) + { + Instruction leaveBodyInstAfterFinally = onProcessExitIl.Create(OpCodes.Leave, ret); + Instruction prevInst = inst.Previous; + onProcessExitMethod.Body.Instructions.Remove(inst); + onProcessExitIl.InsertAfter(prevInst, leaveBodyInstAfterFinally); + } + } + var handler = new ExceptionHandler(ExceptionHandlerType.Finally) + { + TryStart = onProcessExitIl.Body.Instructions.First(), + TryEnd = firstNullParam, + HandlerStart = firstNullParam, + HandlerEnd = ret + }; + + onProcessExitMethod.Body.ExceptionHandlers.Add(handler); + } + + module.Write(stream, new WriterParameters { WriteSymbols = true }); + } + + private void AddCustomModuleTrackerToModule(ModuleDefinition module) + { + using (var coverletInstrumentationAssembly = AssemblyDefinition.ReadAssembly(typeof(ModuleTrackerTemplate).Assembly.Location)) + { + TypeDefinition moduleTrackerTemplate = coverletInstrumentationAssembly.MainModule.GetType( + "Coverlet.Core.Instrumentation", nameof(ModuleTrackerTemplate)); - using var module = ModuleDefinition.ReadModule(stream, parameters); - _reachabilityHelper = ReachabilityHelper.CreateForModule(module, _doesNotReturnAttributes, _logger); + _customTrackerTypeDef = new TypeDefinition( + "Coverlet.Core.Instrumentation.Tracker", Path.GetFileNameWithoutExtension(module.Name) + "_" + _identifier, moduleTrackerTemplate.Attributes); + + _customTrackerTypeDef.BaseType = module.TypeSystem.Object; + foreach (FieldDefinition fieldDef in moduleTrackerTemplate.Fields) + { + var fieldClone = new FieldDefinition(fieldDef.Name, fieldDef.Attributes, fieldDef.FieldType); + fieldClone.FieldType = module.ImportReference(fieldDef.FieldType); + + _customTrackerTypeDef.Fields.Add(fieldClone); + + if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsArray)) + _customTrackerHitsArray = fieldClone; + else if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsFilePath)) + _customTrackerHitsFilePath = fieldClone; + else if (fieldClone.Name == nameof(ModuleTrackerTemplate.SingleHit)) + _customTrackerSingleHit = fieldClone; + else if (fieldClone.Name == nameof(ModuleTrackerTemplate.FlushHitFile)) + _customTrackerFlushHitFile = fieldClone; } - private void InstrumentModule() + foreach (MethodDefinition methodDef in moduleTrackerTemplate.Methods) { - CreateReachabilityHelper(); + var methodOnCustomType = new MethodDefinition(methodDef.Name, methodDef.Attributes, methodDef.ReturnType); - using Stream stream = _fileSystem.NewFileStream(_module, FileMode.Open, FileAccess.ReadWrite); - using var resolver = new NetstandardAwareAssemblyResolver(_module, _logger); - resolver.AddSearchDirectory(Path.GetDirectoryName(_module)); - var parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = resolver }; - if (_isCoreLibrary) - { - parameters.MetadataImporterProvider = new CoreLibMetadataImporterProvider(); - } + foreach (ParameterDefinition parameter in methodDef.Parameters) + { + methodOnCustomType.Parameters.Add(new ParameterDefinition(module.ImportReference(parameter.ParameterType))); + } - using var module = ModuleDefinition.ReadModule(stream, parameters); - foreach (CustomAttribute customAttribute in module.Assembly.CustomAttributes) - { - if (IsExcludeAttribute(customAttribute)) - { - _logger.LogVerbose($"Excluded module: '{module}' for assembly level attribute {customAttribute.AttributeType.FullName}"); - SkipModule = true; - return; - } - } + foreach (VariableDefinition variable in methodDef.Body.Variables) + { + methodOnCustomType.Body.Variables.Add(new VariableDefinition(module.ImportReference(variable.VariableType))); + } - bool containsAppContext = module.GetType(nameof(System), nameof(AppContext)) != null; - IEnumerable types = module.GetTypes(); - AddCustomModuleTrackerToModule(module); + methodOnCustomType.Body.InitLocals = methodDef.Body.InitLocals; - CustomDebugInformation sourceLinkDebugInfo = module.CustomDebugInformations.FirstOrDefault(c => c.Kind == CustomDebugInformationKind.SourceLink); - if (sourceLinkDebugInfo != null) + ILProcessor ilProcessor = methodOnCustomType.Body.GetILProcessor(); + if (methodDef.Name == ".cctor") + _customTrackerClassConstructorIl = ilProcessor; + + foreach (Instruction instr in methodDef.Body.Instructions) + { + if (instr.Operand is MethodReference methodReference) { - _result.SourceLink = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content; - } + if (!methodReference.FullName.Contains(moduleTrackerTemplate.Namespace)) + { + // External method references, just import then + instr.Operand = module.ImportReference(methodReference); + } + else + { + // Move to the custom type + var updatedMethodReference = new MethodReference(methodReference.Name, methodReference.ReturnType, _customTrackerTypeDef); + foreach (ParameterDefinition parameter in methodReference.Parameters) + updatedMethodReference.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes, module.ImportReference(parameter.ParameterType))); - foreach (TypeDefinition type in types) + instr.Operand = updatedMethodReference; + } + } + else if (instr.Operand is FieldReference fieldReference) { - if ( - !Is_System_Threading_Interlocked_CoreLib_Type(type) && - !IsTypeExcluded(type) && - _instrumentationHelper.IsTypeIncluded(_module, type.FullName, _parameters.IncludeFilters) - ) - { - if (IsSynthesizedMemberToBeExcluded(type)) - { - (_excludedCompilerGeneratedTypes ??= new List()).Add(type.FullName); - } - else - { - InstrumentType(type); - } - } + instr.Operand = _customTrackerTypeDef.Fields.Single(fd => fd.Name == fieldReference.Name); } - - // Fixup the custom tracker class constructor, according to all instrumented types - if (_customTrackerRegisterUnloadEventsMethod == null) + else if (instr.Operand is TypeReference typeReference) { - _customTrackerRegisterUnloadEventsMethod = new MethodReference( - nameof(ModuleTrackerTemplate.RegisterUnloadEvents), module.TypeSystem.Void, _customTrackerTypeDef); + instr.Operand = module.ImportReference(typeReference); } - Instruction lastInstr = _customTrackerClassConstructorIl.Body.Instructions.Last(); + ilProcessor.Append(instr); + } - if (!containsAppContext) + foreach (ExceptionHandler handler in methodDef.Body.ExceptionHandlers) + { + if (handler.CatchType != null) { - // For "normal" cases, where the instrumented assembly is not the core library, we add a call to - // RegisterUnloadEvents to the static constructor of the generated custom tracker. Due to static - // initialization constraints, the core library is handled separately below. - _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Call, _customTrackerRegisterUnloadEventsMethod)); + handler.CatchType = module.ImportReference(handler.CatchType); } - _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldc_I4, _result.HitCandidates.Count)); - _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Newarr, module.TypeSystem.Int32)); - _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsArray)); - _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath)); - _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsFilePath)); - _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(_parameters.SingleHit ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0)); - _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerSingleHit)); - _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldc_I4_1)); - _customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerFlushHitFile)); - - if (containsAppContext) - { - // Handle the core library by instrumenting System.AppContext.OnProcessExit to directly call - // the UnloadModule method of the custom tracker type. This avoids loops between the static - // initialization of the custom tracker and the static initialization of the hosting AppDomain - // (which for the core library case will be instrumented code). - var eventArgsType = new TypeReference(nameof(System), nameof(EventArgs), module, module.TypeSystem.CoreLibrary); - var customTrackerUnloadModule = new MethodReference(nameof(ModuleTrackerTemplate.UnloadModule), module.TypeSystem.Void, _customTrackerTypeDef); - customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object)); - customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(eventArgsType)); - - var appContextType = new TypeReference(nameof(System), nameof(AppContext), module, module.TypeSystem.CoreLibrary); - MethodDefinition onProcessExitMethod = new MethodReference("OnProcessExit", module.TypeSystem.Void, appContextType).Resolve(); - ILProcessor onProcessExitIl = onProcessExitMethod.Body.GetILProcessor(); - - // Put the OnProcessExit body inside try/finally to ensure the call to the UnloadModule. - Instruction lastInst = onProcessExitMethod.Body.Instructions.Last(); - var firstNullParam = Instruction.Create(OpCodes.Ldnull); - var secondNullParam = Instruction.Create(OpCodes.Ldnull); - var callUnload = Instruction.Create(OpCodes.Call, customTrackerUnloadModule); - onProcessExitIl.InsertAfter(lastInst, firstNullParam); - onProcessExitIl.InsertAfter(firstNullParam, secondNullParam); - onProcessExitIl.InsertAfter(secondNullParam, callUnload); - var endFinally = Instruction.Create(OpCodes.Endfinally); - onProcessExitIl.InsertAfter(callUnload, endFinally); - Instruction ret = onProcessExitIl.Create(OpCodes.Ret); - Instruction leaveAfterFinally = onProcessExitIl.Create(OpCodes.Leave, ret); - onProcessExitIl.InsertAfter(endFinally, ret); - foreach (Instruction inst in onProcessExitMethod.Body.Instructions.ToArray()) - { - // Patch ret to leave after the finally - if (inst.OpCode == OpCodes.Ret && inst != ret) - { - Instruction leaveBodyInstAfterFinally = onProcessExitIl.Create(OpCodes.Leave, ret); - Instruction prevInst = inst.Previous; - onProcessExitMethod.Body.Instructions.Remove(inst); - onProcessExitIl.InsertAfter(prevInst, leaveBodyInstAfterFinally); - } - } - var handler = new ExceptionHandler(ExceptionHandlerType.Finally) - { - TryStart = onProcessExitIl.Body.Instructions.First(), - TryEnd = firstNullParam, - HandlerStart = firstNullParam, - HandlerEnd = ret - }; - - onProcessExitMethod.Body.ExceptionHandlers.Add(handler); - } + methodOnCustomType.Body.ExceptionHandlers.Add(handler); + } - module.Write(stream, new WriterParameters { WriteSymbols = true }); + _customTrackerTypeDef.Methods.Add(methodOnCustomType); } - private void AddCustomModuleTrackerToModule(ModuleDefinition module) + module.Types.Add(_customTrackerTypeDef); + } + + Debug.Assert(_customTrackerHitsArray != null); + Debug.Assert(_customTrackerClassConstructorIl != null); + } + + private bool IsMethodOfCompilerGeneratedClassOfAsyncStateMachineToBeExcluded(MethodDefinition method) + { + // Type compiler generated, the async state machine + TypeDefinition typeDefinition = method.DeclaringType; + if (typeDefinition.DeclaringType is null) + { + return false; + } + + // Search in type that contains async state machine, compiler generates async state machine in private nested class + foreach (MethodDefinition typeMethod in typeDefinition.DeclaringType.Methods) + { + // If we find the async state machine attribute on method + CustomAttribute attribute; + if ((attribute = typeMethod.CustomAttributes.SingleOrDefault(a => a.AttributeType.FullName == typeof(AsyncStateMachineAttribute).FullName)) != null) { - using (var coverletInstrumentationAssembly = AssemblyDefinition.ReadAssembly(typeof(ModuleTrackerTemplate).Assembly.Location)) + // If the async state machine generated by compiler is "associated" to this method we check for exclusions + // The associated type is specified on attribute constructor + // https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.asyncstatemachineattribute.-ctor?view=netcore-3.1 + if (attribute.ConstructorArguments[0].Value == method.DeclaringType) + { + if (typeMethod.CustomAttributes.Any(IsExcludeAttribute)) { - TypeDefinition moduleTrackerTemplate = coverletInstrumentationAssembly.MainModule.GetType( - "Coverlet.Core.Instrumentation", nameof(ModuleTrackerTemplate)); - - _customTrackerTypeDef = new TypeDefinition( - "Coverlet.Core.Instrumentation.Tracker", Path.GetFileNameWithoutExtension(module.Name) + "_" + _identifier, moduleTrackerTemplate.Attributes); - - _customTrackerTypeDef.BaseType = module.TypeSystem.Object; - foreach (FieldDefinition fieldDef in moduleTrackerTemplate.Fields) - { - var fieldClone = new FieldDefinition(fieldDef.Name, fieldDef.Attributes, fieldDef.FieldType); - fieldClone.FieldType = module.ImportReference(fieldDef.FieldType); - - _customTrackerTypeDef.Fields.Add(fieldClone); - - if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsArray)) - _customTrackerHitsArray = fieldClone; - else if (fieldClone.Name == nameof(ModuleTrackerTemplate.HitsFilePath)) - _customTrackerHitsFilePath = fieldClone; - else if (fieldClone.Name == nameof(ModuleTrackerTemplate.SingleHit)) - _customTrackerSingleHit = fieldClone; - else if (fieldClone.Name == nameof(ModuleTrackerTemplate.FlushHitFile)) - _customTrackerFlushHitFile = fieldClone; - } - - foreach (MethodDefinition methodDef in moduleTrackerTemplate.Methods) - { - var methodOnCustomType = new MethodDefinition(methodDef.Name, methodDef.Attributes, methodDef.ReturnType); - - foreach (ParameterDefinition parameter in methodDef.Parameters) - { - methodOnCustomType.Parameters.Add(new ParameterDefinition(module.ImportReference(parameter.ParameterType))); - } - - foreach (VariableDefinition variable in methodDef.Body.Variables) - { - methodOnCustomType.Body.Variables.Add(new VariableDefinition(module.ImportReference(variable.VariableType))); - } - - methodOnCustomType.Body.InitLocals = methodDef.Body.InitLocals; - - ILProcessor ilProcessor = methodOnCustomType.Body.GetILProcessor(); - if (methodDef.Name == ".cctor") - _customTrackerClassConstructorIl = ilProcessor; - - foreach (Instruction instr in methodDef.Body.Instructions) - { - if (instr.Operand is MethodReference methodReference) - { - if (!methodReference.FullName.Contains(moduleTrackerTemplate.Namespace)) - { - // External method references, just import then - instr.Operand = module.ImportReference(methodReference); - } - else - { - // Move to the custom type - var updatedMethodReference = new MethodReference(methodReference.Name, methodReference.ReturnType, _customTrackerTypeDef); - foreach (ParameterDefinition parameter in methodReference.Parameters) - updatedMethodReference.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes, module.ImportReference(parameter.ParameterType))); - - instr.Operand = updatedMethodReference; - } - } - else if (instr.Operand is FieldReference fieldReference) - { - instr.Operand = _customTrackerTypeDef.Fields.Single(fd => fd.Name == fieldReference.Name); - } - else if (instr.Operand is TypeReference typeReference) - { - instr.Operand = module.ImportReference(typeReference); - } - - ilProcessor.Append(instr); - } - - foreach (ExceptionHandler handler in methodDef.Body.ExceptionHandlers) - { - if (handler.CatchType != null) - { - handler.CatchType = module.ImportReference(handler.CatchType); - } - - methodOnCustomType.Body.ExceptionHandlers.Add(handler); - } - - _customTrackerTypeDef.Methods.Add(methodOnCustomType); - } - - module.Types.Add(_customTrackerTypeDef); + return true; } - - Debug.Assert(_customTrackerHitsArray != null); - Debug.Assert(_customTrackerClassConstructorIl != null); + } } + } + return false; + } - private bool IsMethodOfCompilerGeneratedClassOfAsyncStateMachineToBeExcluded(MethodDefinition method) + private void InstrumentType(TypeDefinition type) + { + IEnumerable methods = type.GetMethods(); + + // We keep ordinal index because it's the way used by compiler for generated types/methods to + // avoid ambiguity + int ordinal = -1; + foreach (MethodDefinition method in methods) + { + MethodDefinition actualMethod = method; + IEnumerable customAttributes = method.CustomAttributes; + if (_instrumentationHelper.IsLocalMethod(method.Name)) + actualMethod = methods.FirstOrDefault(m => m.Name == method.Name.Split('>')[0].Substring(1)) ?? method; + + if (actualMethod.IsGetter || actualMethod.IsSetter) { - // Type compiler generated, the async state machine - TypeDefinition typeDefinition = method.DeclaringType; - if (typeDefinition.DeclaringType is null) - { - return false; - } + if (_parameters.SkipAutoProps && actualMethod.CustomAttributes.Any(ca => ca.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName)) + { + continue; + } + + PropertyDefinition prop = type.Properties.FirstOrDefault(p => p.GetMethod?.FullName.Equals(actualMethod.FullName) == true || + p.SetMethod?.FullName.Equals(actualMethod.FullName) == true); + if (prop?.HasCustomAttributes == true) + customAttributes = customAttributes.Union(prop.CustomAttributes); + } - // Search in type that contains async state machine, compiler generates async state machine in private nested class - foreach (MethodDefinition typeMethod in typeDefinition.DeclaringType.Methods) - { - // If we find the async state machine attribute on method - CustomAttribute attribute; - if ((attribute = typeMethod.CustomAttributes.SingleOrDefault(a => a.AttributeType.FullName == typeof(AsyncStateMachineAttribute).FullName)) != null) - { - // If the async state machine generated by compiler is "associated" to this method we check for exclusions - // The associated type is specified on attribute constructor - // https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.asyncstatemachineattribute.-ctor?view=netcore-3.1 - if (attribute.ConstructorArguments[0].Value == method.DeclaringType) - { - if (typeMethod.CustomAttributes.Any(IsExcludeAttribute)) - { - return true; - } - } - } - } - return false; + ordinal++; + + if (IsMethodOfCompilerGeneratedClassOfAsyncStateMachineToBeExcluded(method)) + { + continue; } - private void InstrumentType(TypeDefinition type) + if (IsSynthesizedMemberToBeExcluded(method)) { - IEnumerable methods = type.GetMethods(); + continue; + } - // We keep ordinal index because it's the way used by compiler for generated types/methods to - // avoid ambiguity - int ordinal = -1; - foreach (MethodDefinition method in methods) - { - MethodDefinition actualMethod = method; - IEnumerable customAttributes = method.CustomAttributes; - if (_instrumentationHelper.IsLocalMethod(method.Name)) - actualMethod = methods.FirstOrDefault(m => m.Name == method.Name.Split('>')[0].Substring(1)) ?? method; - - if (actualMethod.IsGetter || actualMethod.IsSetter) - { - if (_parameters.SkipAutoProps && actualMethod.CustomAttributes.Any(ca => ca.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName)) - { - continue; - } - - PropertyDefinition prop = type.Properties.FirstOrDefault(p => p.GetMethod?.FullName.Equals(actualMethod.FullName) == true || - p.SetMethod?.FullName.Equals(actualMethod.FullName) == true); - if (prop?.HasCustomAttributes == true) - customAttributes = customAttributes.Union(prop.CustomAttributes); - } - - ordinal++; - - if (IsMethodOfCompilerGeneratedClassOfAsyncStateMachineToBeExcluded(method)) - { - continue; - } - - if (IsSynthesizedMemberToBeExcluded(method)) - { - continue; - } - - if (_excludedLambdaMethods != null && _excludedLambdaMethods.Contains(method.FullName)) - { - continue; - } - - if (!customAttributes.Any(IsExcludeAttribute)) - { - InstrumentMethod(method); - } - else - { - (_excludedLambdaMethods ??= new List()).AddRange(CollectLambdaMethodsInsideLocalFunction(method)); - (_excludedMethods ??= new List<(MethodDefinition, int)>()).Add((method, ordinal)); - } - } + if (_excludedLambdaMethods != null && _excludedLambdaMethods.Contains(method.FullName)) + { + continue; + } - IEnumerable ctors = type.GetConstructors(); - foreach (MethodDefinition ctor in ctors) - { - if (!ctor.CustomAttributes.Any(IsExcludeAttribute)) - { - InstrumentMethod(ctor); - } - } + if (!customAttributes.Any(IsExcludeAttribute)) + { + InstrumentMethod(method); + } + else + { + (_excludedLambdaMethods ??= new List()).AddRange(CollectLambdaMethodsInsideLocalFunction(method)); + (_excludedMethods ??= new List<(MethodDefinition, int)>()).Add((method, ordinal)); } + } - private void InstrumentMethod(MethodDefinition method) + IEnumerable ctors = type.GetConstructors(); + foreach (MethodDefinition ctor in ctors) + { + if (!ctor.CustomAttributes.Any(IsExcludeAttribute)) { - string sourceFile = method.DebugInformation.SequencePoints.Select(s => _sourceRootTranslator.ResolveFilePath(s.Document.Url)).FirstOrDefault(); + InstrumentMethod(ctor); + } + } + } - if (string.IsNullOrEmpty(sourceFile)) return; + private void InstrumentMethod(MethodDefinition method) + { + string sourceFile = method.DebugInformation.SequencePoints.Select(s => _sourceRootTranslator.ResolveFilePath(s.Document.Url)).FirstOrDefault(); - if (!string.IsNullOrEmpty(sourceFile) && _excludedFilesHelper.Exclude(sourceFile)) - { - if (!(_excludedSourceFiles ??= new List()).Contains(sourceFile)) - { - _excludedSourceFiles.Add(sourceFile); - } - return; - } + if (string.IsNullOrEmpty(sourceFile)) return; - MethodBody methodBody = GetMethodBody(method); - if (methodBody == null) - return; - - if (method.IsNative) - return; - - InstrumentIL(method); - } - - /// - /// The base idea is to inject an int placeholder for every sequence point. We register source+placeholder+lines(from sequence point) for final accounting. - /// Instrumentation alg(current instruction: instruction we're analyzing): - /// 1) We get all branches for the method - /// 2) We get the sequence point of every instruction of method(start line/end line) - /// 3) We check if current instruction is reachable and coverable - /// 4) For every sequence point of an instruction we put load(int hint placeholder)+call opcode above current instruction - /// 5) We patch all jump to current instruction with first injected instruction(load) - /// 6) If current instruction is a target for a branch we inject again load(int hint placeholder)+call opcode above current instruction - /// 7) We patch all jump to current instruction with first injected instruction(load) - /// - private void InstrumentIL(MethodDefinition method) - { - method.Body.SimplifyMacros(); - ILProcessor processor = method.Body.GetILProcessor(); - int index = 0; - int count = processor.Body.Instructions.Count; - IReadOnlyList branchPoints = _cecilSymbolHelper.GetBranchPoints(method); - IDictionary targetsMap = new Dictionary(); - System.Collections.Immutable.ImmutableArray unreachableRanges = _reachabilityHelper.FindUnreachableIL(processor.Body.Instructions, processor.Body.ExceptionHandlers); - int currentUnreachableRangeIx = 0; - for (int n = 0; n < count; n++) - { - Instruction currentInstruction = processor.Body.Instructions[index]; - SequencePoint sequencePoint = method.DebugInformation.GetSequencePoint(currentInstruction); - IEnumerable targetedBranchPoints = branchPoints.Where(p => p.EndOffset == currentInstruction.Offset); - - // make sure we're looking at the correct unreachable range (if any) - int instrOffset = currentInstruction.Offset; - while (currentUnreachableRangeIx < unreachableRanges.Length && instrOffset > unreachableRanges[currentUnreachableRangeIx].EndOffset) - { - currentUnreachableRangeIx++; - } - - // determine if the unreachable - bool isUnreachable = false; - if (currentUnreachableRangeIx < unreachableRanges.Length) - { - ReachabilityHelper.UnreachableRange range = unreachableRanges[currentUnreachableRangeIx]; - isUnreachable = instrOffset >= range.StartOffset && instrOffset <= range.EndOffset; - } - - // Check is both reachable, _and_ coverable - if (isUnreachable || _cecilSymbolHelper.SkipNotCoverableInstruction(method, currentInstruction)) - { - index++; - continue; - } - - if (sequencePoint != null && !sequencePoint.IsHidden) - { - if (_cecilSymbolHelper.SkipInlineAssignedAutoProperty(_parameters.SkipAutoProps, method, currentInstruction)) - { - index++; - continue; - } - - Instruction firstInjectedInstrumentedOpCode = AddInstrumentationCode(method, processor, currentInstruction, sequencePoint); - targetsMap.Add(currentInstruction.Offset, firstInjectedInstrumentedOpCode); - - index += 2; - } - - foreach (BranchPoint branchTarget in targetedBranchPoints) - { - /* - * Skip branches with no sequence point reference for now. - * In this case for an anonymous class the compiler will dynamically create an Equals 'utility' method. - * The CecilSymbolHelper will create branch points with a start line of -1 and no document, which - * I am currently not sure how to handle. - */ - if (branchTarget.StartLine == -1 || branchTarget.Document == null) - continue; - - Instruction firstInjectedInstrumentedOpCode = AddInstrumentationCode(method, processor, currentInstruction, branchTarget); - if (!targetsMap.ContainsKey(currentInstruction.Offset)) - targetsMap.Add(currentInstruction.Offset, firstInjectedInstrumentedOpCode); - - index += 2; - } - - index++; - } + if (!string.IsNullOrEmpty(sourceFile) && _excludedFilesHelper.Exclude(sourceFile)) + { + if (!(_excludedSourceFiles ??= new List()).Contains(sourceFile)) + { + _excludedSourceFiles.Add(sourceFile); + } + return; + } + + MethodBody methodBody = GetMethodBody(method); + if (methodBody == null) + return; + + if (method.IsNative) + return; - foreach (Instruction bodyInstruction in processor.Body.Instructions) - ReplaceInstructionTarget(bodyInstruction, targetsMap); + InstrumentIL(method); + } - foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) - ReplaceExceptionHandlerBoundary(handler, targetsMap); + /// + /// The base idea is to inject an int placeholder for every sequence point. We register source+placeholder+lines(from sequence point) for final accounting. + /// Instrumentation alg(current instruction: instruction we're analyzing): + /// 1) We get all branches for the method + /// 2) We get the sequence point of every instruction of method(start line/end line) + /// 3) We check if current instruction is reachable and coverable + /// 4) For every sequence point of an instruction we put load(int hint placeholder)+call opcode above current instruction + /// 5) We patch all jump to current instruction with first injected instruction(load) + /// 6) If current instruction is a target for a branch we inject again load(int hint placeholder)+call opcode above current instruction + /// 7) We patch all jump to current instruction with first injected instruction(load) + /// + private void InstrumentIL(MethodDefinition method) + { + method.Body.SimplifyMacros(); + ILProcessor processor = method.Body.GetILProcessor(); + int index = 0; + int count = processor.Body.Instructions.Count; + IReadOnlyList branchPoints = _cecilSymbolHelper.GetBranchPoints(method); + IDictionary targetsMap = new Dictionary(); + System.Collections.Immutable.ImmutableArray unreachableRanges = _reachabilityHelper.FindUnreachableIL(processor.Body.Instructions, processor.Body.ExceptionHandlers); + int currentUnreachableRangeIx = 0; + for (int n = 0; n < count; n++) + { + Instruction currentInstruction = processor.Body.Instructions[index]; + SequencePoint sequencePoint = method.DebugInformation.GetSequencePoint(currentInstruction); + IEnumerable targetedBranchPoints = branchPoints.Where(p => p.EndOffset == currentInstruction.Offset); + + // make sure we're looking at the correct unreachable range (if any) + int instrOffset = currentInstruction.Offset; + while (currentUnreachableRangeIx < unreachableRanges.Length && instrOffset > unreachableRanges[currentUnreachableRangeIx].EndOffset) + { + currentUnreachableRangeIx++; + } - method.Body.OptimizeMacros(); + // determine if the unreachable + bool isUnreachable = false; + if (currentUnreachableRangeIx < unreachableRanges.Length) + { + ReachabilityHelper.UnreachableRange range = unreachableRanges[currentUnreachableRangeIx]; + isUnreachable = instrOffset >= range.StartOffset && instrOffset <= range.EndOffset; } - private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, SequencePoint sequencePoint) + // Check is both reachable, _and_ coverable + if (isUnreachable || _cecilSymbolHelper.SkipNotCoverableInstruction(method, currentInstruction)) { - if (!_result.Documents.TryGetValue(_sourceRootTranslator.ResolveFilePath(sequencePoint.Document.Url), out Document document)) - { - document = new Document { Path = _sourceRootTranslator.ResolveFilePath(sequencePoint.Document.Url) }; - document.Index = _result.Documents.Count; - _result.Documents.Add(document.Path, document); - } + index++; + continue; + } - for (int i = sequencePoint.StartLine; i <= sequencePoint.EndLine; i++) - { - if (!document.Lines.ContainsKey(i)) - document.Lines.Add(i, new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName }); - } + if (sequencePoint != null && !sequencePoint.IsHidden) + { + if (_cecilSymbolHelper.SkipInlineAssignedAutoProperty(_parameters.SkipAutoProps, method, currentInstruction)) + { + index++; + continue; + } - _result.HitCandidates.Add(new HitCandidate(false, document.Index, sequencePoint.StartLine, sequencePoint.EndLine)); + Instruction firstInjectedInstrumentedOpCode = AddInstrumentationCode(method, processor, currentInstruction, sequencePoint); + targetsMap.Add(currentInstruction.Offset, firstInjectedInstrumentedOpCode); - return AddInstrumentationInstructions(method, processor, instruction, _result.HitCandidates.Count - 1); + index += 2; } - private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, BranchPoint branchPoint) + foreach (BranchPoint branchTarget in targetedBranchPoints) { - if (!_result.Documents.TryGetValue(_sourceRootTranslator.ResolveFilePath(branchPoint.Document), out Document document)) - { - document = new Document { Path = _sourceRootTranslator.ResolveFilePath(branchPoint.Document) }; - document.Index = _result.Documents.Count; - _result.Documents.Add(document.Path, document); - } + /* + * Skip branches with no sequence point reference for now. + * In this case for an anonymous class the compiler will dynamically create an Equals 'utility' method. + * The CecilSymbolHelper will create branch points with a start line of -1 and no document, which + * I am currently not sure how to handle. + */ + if (branchTarget.StartLine == -1 || branchTarget.Document == null) + continue; + + Instruction firstInjectedInstrumentedOpCode = AddInstrumentationCode(method, processor, currentInstruction, branchTarget); + if (!targetsMap.ContainsKey(currentInstruction.Offset)) + targetsMap.Add(currentInstruction.Offset, firstInjectedInstrumentedOpCode); + + index += 2; + } - var key = new BranchKey(branchPoint.StartLine, (int)branchPoint.Ordinal); - if (!document.Branches.ContainsKey(key)) - { - document.Branches.Add( - key, - new Branch - { - Number = branchPoint.StartLine, - Class = method.DeclaringType.FullName, - Method = method.FullName, - Offset = branchPoint.Offset, - EndOffset = branchPoint.EndOffset, - Path = branchPoint.Path, - Ordinal = branchPoint.Ordinal - } - ); - - if (method.DeclaringType.CustomAttributes.Any(x => x.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName)) - { - if (_branchesInCompiledGeneratedClass == null) - { - _branchesInCompiledGeneratedClass = new List(); - } - - if (!_branchesInCompiledGeneratedClass.Contains(method.FullName)) - { - _branchesInCompiledGeneratedClass.Add(method.FullName); - } - } - } + index++; + } - _result.HitCandidates.Add(new HitCandidate(true, document.Index, branchPoint.StartLine, (int)branchPoint.Ordinal)); + foreach (Instruction bodyInstruction in processor.Body.Instructions) + ReplaceInstructionTarget(bodyInstruction, targetsMap); - return AddInstrumentationInstructions(method, processor, instruction, _result.HitCandidates.Count - 1); - } + foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) + ReplaceExceptionHandlerBoundary(handler, targetsMap); + + method.Body.OptimizeMacros(); + } + + private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, SequencePoint sequencePoint) + { + if (!_result.Documents.TryGetValue(_sourceRootTranslator.ResolveFilePath(sequencePoint.Document.Url), out Document document)) + { + document = new Document { Path = _sourceRootTranslator.ResolveFilePath(sequencePoint.Document.Url) }; + document.Index = _result.Documents.Count; + _result.Documents.Add(document.Path, document); + } + + for (int i = sequencePoint.StartLine; i <= sequencePoint.EndLine; i++) + { + if (!document.Lines.ContainsKey(i)) + document.Lines.Add(i, new Line { Number = i, Class = method.DeclaringType.FullName, Method = method.FullName }); + } + + _result.HitCandidates.Add(new HitCandidate(false, document.Index, sequencePoint.StartLine, sequencePoint.EndLine)); + + return AddInstrumentationInstructions(method, processor, instruction, _result.HitCandidates.Count - 1); + } - private Instruction AddInstrumentationInstructions(MethodDefinition method, ILProcessor processor, Instruction instruction, int hitEntryIndex) + private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, BranchPoint branchPoint) + { + if (!_result.Documents.TryGetValue(_sourceRootTranslator.ResolveFilePath(branchPoint.Document), out Document document)) + { + document = new Document { Path = _sourceRootTranslator.ResolveFilePath(branchPoint.Document) }; + document.Index = _result.Documents.Count; + _result.Documents.Add(document.Path, document); + } + + var key = new BranchKey(branchPoint.StartLine, (int)branchPoint.Ordinal); + if (!document.Branches.ContainsKey(key)) + { + document.Branches.Add( + key, + new Branch + { + Number = branchPoint.StartLine, + Class = method.DeclaringType.FullName, + Method = method.FullName, + Offset = branchPoint.Offset, + EndOffset = branchPoint.EndOffset, + Path = branchPoint.Path, + Ordinal = branchPoint.Ordinal + } + ); + + if (method.DeclaringType.CustomAttributes.Any(x => x.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName)) { - if (_customTrackerRecordHitMethod == null) - { - string recordHitMethodName; - if (_parameters.SingleHit) - { - recordHitMethodName = _isCoreLibrary - ? nameof(ModuleTrackerTemplate.RecordSingleHitInCoreLibrary) - : nameof(ModuleTrackerTemplate.RecordSingleHit); - } - else - { - recordHitMethodName = _isCoreLibrary - ? nameof(ModuleTrackerTemplate.RecordHitInCoreLibrary) - : nameof(ModuleTrackerTemplate.RecordHit); - } - - _customTrackerRecordHitMethod = new MethodReference( - recordHitMethodName, method.Module.TypeSystem.Void, _customTrackerTypeDef); - _customTrackerRecordHitMethod.Parameters.Add(new ParameterDefinition("hitLocationIndex", ParameterAttributes.None, method.Module.TypeSystem.Int32)); - } + if (_branchesInCompiledGeneratedClass == null) + { + _branchesInCompiledGeneratedClass = new List(); + } + + if (!_branchesInCompiledGeneratedClass.Contains(method.FullName)) + { + _branchesInCompiledGeneratedClass.Add(method.FullName); + } + } + } - var indxInstr = Instruction.Create(OpCodes.Ldc_I4, hitEntryIndex); - var callInstr = Instruction.Create(OpCodes.Call, _customTrackerRecordHitMethod); + _result.HitCandidates.Add(new HitCandidate(true, document.Index, branchPoint.StartLine, (int)branchPoint.Ordinal)); - processor.InsertBefore(instruction, callInstr); - processor.InsertBefore(callInstr, indxInstr); + return AddInstrumentationInstructions(method, processor, instruction, _result.HitCandidates.Count - 1); + } - return indxInstr; + private Instruction AddInstrumentationInstructions(MethodDefinition method, ILProcessor processor, Instruction instruction, int hitEntryIndex) + { + if (_customTrackerRecordHitMethod == null) + { + string recordHitMethodName; + if (_parameters.SingleHit) + { + recordHitMethodName = _isCoreLibrary + ? nameof(ModuleTrackerTemplate.RecordSingleHitInCoreLibrary) + : nameof(ModuleTrackerTemplate.RecordSingleHit); } - - private static void ReplaceInstructionTarget(Instruction instruction, IDictionary targetsMap) + else { - if (instruction.Operand is Instruction operandInstruction) - { - if (targetsMap.TryGetValue(operandInstruction.Offset, out Instruction newTarget)) - { - instruction.Operand = newTarget; - } - } - else if (instruction.Operand is Instruction[] operandInstructions) - { - for (int i = 0; i < operandInstructions.Length; i++) - { - if (targetsMap.TryGetValue(operandInstructions[i].Offset, out Instruction newTarget)) - operandInstructions[i] = newTarget; - } - } + recordHitMethodName = _isCoreLibrary + ? nameof(ModuleTrackerTemplate.RecordHitInCoreLibrary) + : nameof(ModuleTrackerTemplate.RecordHit); } - private static void ReplaceExceptionHandlerBoundary(ExceptionHandler handler, IDictionary targetsMap) - { - if (handler.FilterStart is not null && targetsMap.TryGetValue(handler.FilterStart.Offset, out Instruction newFilterStart)) - handler.FilterStart = newFilterStart; + _customTrackerRecordHitMethod = new MethodReference( + recordHitMethodName, method.Module.TypeSystem.Void, _customTrackerTypeDef); + _customTrackerRecordHitMethod.Parameters.Add(new ParameterDefinition("hitLocationIndex", ParameterAttributes.None, method.Module.TypeSystem.Int32)); + } - if (handler.HandlerEnd is not null && targetsMap.TryGetValue(handler.HandlerEnd.Offset, out Instruction newHandlerEnd)) - handler.HandlerEnd = newHandlerEnd; + var indxInstr = Instruction.Create(OpCodes.Ldc_I4, hitEntryIndex); + var callInstr = Instruction.Create(OpCodes.Call, _customTrackerRecordHitMethod); - if (handler.HandlerStart is not null && targetsMap.TryGetValue(handler.HandlerStart.Offset, out Instruction newHandlerStart)) - handler.HandlerStart = newHandlerStart; + processor.InsertBefore(instruction, callInstr); + processor.InsertBefore(callInstr, indxInstr); - if (handler.TryEnd is not null && targetsMap.TryGetValue(handler.TryEnd.Offset, out Instruction newTryEnd)) - handler.TryEnd = newTryEnd; + return indxInstr; + } - if (handler.TryStart is not null && targetsMap.TryGetValue(handler.TryStart.Offset, out Instruction newTryStart)) - handler.TryStart = newTryStart; + private static void ReplaceInstructionTarget(Instruction instruction, IDictionary targetsMap) + { + if (instruction.Operand is Instruction operandInstruction) + { + if (targetsMap.TryGetValue(operandInstruction.Offset, out Instruction newTarget)) + { + instruction.Operand = newTarget; } - - private bool IsExcludeAttribute(CustomAttribute customAttribute) + } + else if (instruction.Operand is Instruction[] operandInstructions) + { + for (int i = 0; i < operandInstructions.Length; i++) { - return Array.IndexOf(_excludedAttributes, customAttribute.AttributeType.Name) != -1; + if (targetsMap.TryGetValue(operandInstructions[i].Offset, out Instruction newTarget)) + operandInstructions[i] = newTarget; } + } + } + + private static void ReplaceExceptionHandlerBoundary(ExceptionHandler handler, IDictionary targetsMap) + { + if (handler.FilterStart is not null && targetsMap.TryGetValue(handler.FilterStart.Offset, out Instruction newFilterStart)) + handler.FilterStart = newFilterStart; - private static MethodBody GetMethodBody(MethodDefinition method) + if (handler.HandlerEnd is not null && targetsMap.TryGetValue(handler.HandlerEnd.Offset, out Instruction newHandlerEnd)) + handler.HandlerEnd = newHandlerEnd; + + if (handler.HandlerStart is not null && targetsMap.TryGetValue(handler.HandlerStart.Offset, out Instruction newHandlerStart)) + handler.HandlerStart = newHandlerStart; + + if (handler.TryEnd is not null && targetsMap.TryGetValue(handler.TryEnd.Offset, out Instruction newTryEnd)) + handler.TryEnd = newTryEnd; + + if (handler.TryStart is not null && targetsMap.TryGetValue(handler.TryStart.Offset, out Instruction newTryStart)) + handler.TryStart = newTryStart; + } + + private bool IsExcludeAttribute(CustomAttribute customAttribute) + { + return Array.IndexOf(_excludedAttributes, customAttribute.AttributeType.Name) != -1; + } + + private static MethodBody GetMethodBody(MethodDefinition method) + { + try + { + return method.HasBody ? method.Body : null; + } + catch + { + return null; + } + } + + // Check if the member (type or method) is generated by the compiler from a method excluded from code coverage + private bool IsSynthesizedMemberToBeExcluded(IMemberDefinition definition) + { + if (_excludedMethods is null) + { + return false; + } + + TypeDefinition declaringType = definition.DeclaringType; + + // We check all parent type of current one bottom-up + while (declaringType != null) + { + + // If parent type is excluded return + if (_excludedCompilerGeneratedTypes != null && + _excludedCompilerGeneratedTypes.Any(t => t == declaringType.FullName)) { - try - { - return method.HasBody ? method.Body : null; - } - catch - { - return null; - } + return true; } - // Check if the member (type or method) is generated by the compiler from a method excluded from code coverage - private bool IsSynthesizedMemberToBeExcluded(IMemberDefinition definition) + // Check methods members and compiler generated types + foreach ((MethodDefinition, int) excludedMethods in _excludedMethods) { - if (_excludedMethods is null) - { - return false; - } + // Exclude this member if declaring type is the same of the excluded method and + // the name is synthesized from the name of the excluded method. + // + if (declaringType.FullName == excludedMethods.Item1.DeclaringType.FullName && + IsSynthesizedNameOf(definition.Name, excludedMethods.Item1.Name, excludedMethods.Item2)) + { + return true; + } + } + declaringType = declaringType.DeclaringType; + } - TypeDefinition declaringType = definition.DeclaringType; + return false; + } - // We check all parent type of current one bottom-up - while (declaringType != null) - { + // Check if the name is synthesized by the compiler + // Refer to https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs + // to see how the compiler generate names for lambda, local function, yield or async/await expressions + internal bool IsSynthesizedNameOf(string name, string methodName, int methodOrdinal) + { + return + // Lambda method + name.IndexOf($"<{methodName}>b__{methodOrdinal}") != -1 || + // Lambda display class + name.IndexOf($"<>c__DisplayClass{methodOrdinal}_") != -1 || + // State machine + name.IndexOf($"<{methodName}>d__{methodOrdinal}") != -1 || + // Local function + (name.IndexOf($"<{methodName}>g__") != -1 && name.IndexOf($"|{methodOrdinal}_") != -1); + } - // If parent type is excluded return - if (_excludedCompilerGeneratedTypes != null && - _excludedCompilerGeneratedTypes.Any(t => t == declaringType.FullName)) - { - return true; - } - - // Check methods members and compiler generated types - foreach ((MethodDefinition, int) excludedMethods in _excludedMethods) - { - // Exclude this member if declaring type is the same of the excluded method and - // the name is synthesized from the name of the excluded method. - // - if (declaringType.FullName == excludedMethods.Item1.DeclaringType.FullName && - IsSynthesizedNameOf(definition.Name, excludedMethods.Item1.Name, excludedMethods.Item2)) - { - return true; - } - } - declaringType = declaringType.DeclaringType; - } + private static IEnumerable CollectLambdaMethodsInsideLocalFunction(MethodDefinition methodDefinition) + { + if (!methodDefinition.Name.Contains(">g__")) yield break; - return false; + foreach (Instruction instruction in methodDefinition.Body.Instructions.ToList()) + { + if (instruction.OpCode == OpCodes.Ldftn && instruction.Operand is MethodReference mr && mr.Name.Contains(">b__")) + { + yield return mr.FullName; } + } + } - // Check if the name is synthesized by the compiler - // Refer to https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs - // to see how the compiler generate names for lambda, local function, yield or async/await expressions - internal bool IsSynthesizedNameOf(string name, string methodName, int methodOrdinal) + /// + /// A custom importer created specifically to allow the instrumentation of System.Private.CoreLib by + /// removing the external references to netstandard that are generated when instrumenting a typical + /// assembly. + /// + private class CoreLibMetadataImporterProvider : IMetadataImporterProvider + { + public IMetadataImporter GetMetadataImporter(ModuleDefinition module) + { + return new CoreLibMetadataImporter(module); + } + + private class CoreLibMetadataImporter : IMetadataImporter + { + private readonly ModuleDefinition _module; + private readonly DefaultMetadataImporter _defaultMetadataImporter; + + public CoreLibMetadataImporter(ModuleDefinition module) { - return - // Lambda method - name.IndexOf($"<{methodName}>b__{methodOrdinal}") != -1 || - // Lambda display class - name.IndexOf($"<>c__DisplayClass{methodOrdinal}_") != -1 || - // State machine - name.IndexOf($"<{methodName}>d__{methodOrdinal}") != -1 || - // Local function - (name.IndexOf($"<{methodName}>g__") != -1 && name.IndexOf($"|{methodOrdinal}_") != -1); + _module = module; + _defaultMetadataImporter = new DefaultMetadataImporter(module); } - private static IEnumerable CollectLambdaMethodsInsideLocalFunction(MethodDefinition methodDefinition) + public AssemblyNameReference ImportReference(AssemblyNameReference reference) { - if (!methodDefinition.Name.Contains(">g__")) yield break; + return _defaultMetadataImporter.ImportReference(reference); + } - foreach (Instruction instruction in methodDefinition.Body.Instructions.ToList()) - { - if (instruction.OpCode == OpCodes.Ldftn && instruction.Operand is MethodReference mr && mr.Name.Contains(">b__")) - { - yield return mr.FullName; - } - } + public TypeReference ImportReference(TypeReference type, IGenericParameterProvider context) + { + TypeReference importedRef = _defaultMetadataImporter.ImportReference(type, context); + importedRef.GetElementType().Scope = _module.TypeSystem.CoreLibrary; + return importedRef; } - /// - /// A custom importer created specifically to allow the instrumentation of System.Private.CoreLib by - /// removing the external references to netstandard that are generated when instrumenting a typical - /// assembly. - /// - private class CoreLibMetadataImporterProvider : IMetadataImporterProvider + public FieldReference ImportReference(FieldReference field, IGenericParameterProvider context) { - public IMetadataImporter GetMetadataImporter(ModuleDefinition module) - { - return new CoreLibMetadataImporter(module); - } + FieldReference importedRef = _defaultMetadataImporter.ImportReference(field, context); + importedRef.FieldType.GetElementType().Scope = _module.TypeSystem.CoreLibrary; + return importedRef; + } + + public MethodReference ImportReference(MethodReference method, IGenericParameterProvider context) + { + MethodReference importedRef = _defaultMetadataImporter.ImportReference(method, context); + importedRef.DeclaringType.GetElementType().Scope = _module.TypeSystem.CoreLibrary; - private class CoreLibMetadataImporter : IMetadataImporter + foreach (ParameterDefinition parameter in importedRef.Parameters) + { + if (parameter.ParameterType.Scope == _module.TypeSystem.CoreLibrary) { - private readonly ModuleDefinition _module; - private readonly DefaultMetadataImporter _defaultMetadataImporter; - - public CoreLibMetadataImporter(ModuleDefinition module) - { - _module = module; - _defaultMetadataImporter = new DefaultMetadataImporter(module); - } - - public AssemblyNameReference ImportReference(AssemblyNameReference reference) - { - return _defaultMetadataImporter.ImportReference(reference); - } - - public TypeReference ImportReference(TypeReference type, IGenericParameterProvider context) - { - TypeReference importedRef = _defaultMetadataImporter.ImportReference(type, context); - importedRef.GetElementType().Scope = _module.TypeSystem.CoreLibrary; - return importedRef; - } - - public FieldReference ImportReference(FieldReference field, IGenericParameterProvider context) - { - FieldReference importedRef = _defaultMetadataImporter.ImportReference(field, context); - importedRef.FieldType.GetElementType().Scope = _module.TypeSystem.CoreLibrary; - return importedRef; - } - - public MethodReference ImportReference(MethodReference method, IGenericParameterProvider context) - { - MethodReference importedRef = _defaultMetadataImporter.ImportReference(method, context); - importedRef.DeclaringType.GetElementType().Scope = _module.TypeSystem.CoreLibrary; - - foreach (ParameterDefinition parameter in importedRef.Parameters) - { - if (parameter.ParameterType.Scope == _module.TypeSystem.CoreLibrary) - { - continue; - } - - parameter.ParameterType.GetElementType().Scope = _module.TypeSystem.CoreLibrary; - } - - if (importedRef.ReturnType.Scope != _module.TypeSystem.CoreLibrary) - { - importedRef.ReturnType.GetElementType().Scope = _module.TypeSystem.CoreLibrary; - } - - return importedRef; - } + continue; } + + parameter.ParameterType.GetElementType().Scope = _module.TypeSystem.CoreLibrary; + } + + if (importedRef.ReturnType.Scope != _module.TypeSystem.CoreLibrary) + { + importedRef.ReturnType.GetElementType().Scope = _module.TypeSystem.CoreLibrary; + } + + return importedRef; } + } } + } - // Exclude files helper https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.filesystemglobbing.matcher?view=aspnetcore-2.2 - internal class ExcludedFilesHelper - { - readonly Matcher _matcher; + // Exclude files helper https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.filesystemglobbing.matcher?view=aspnetcore-2.2 + internal class ExcludedFilesHelper + { + readonly Matcher _matcher; - public ExcludedFilesHelper(string[] excludes, ILogger logger) + public ExcludedFilesHelper(string[] excludes, ILogger logger) + { + if (excludes != null && excludes.Length > 0) + { + _matcher = new Matcher(); + foreach (string excludeRule in excludes) { - if (excludes != null && excludes.Length > 0) - { - _matcher = new Matcher(); - foreach (string excludeRule in excludes) - { - if (excludeRule is null) - { - continue; - } - _matcher.AddInclude(Path.IsPathRooted(excludeRule) ? excludeRule.Substring(Path.GetPathRoot(excludeRule).Length) : excludeRule); - } - } + if (excludeRule is null) + { + continue; + } + _matcher.AddInclude(Path.IsPathRooted(excludeRule) ? excludeRule.Substring(Path.GetPathRoot(excludeRule).Length) : excludeRule); } + } + } - public bool Exclude(string sourceFile) - { - if (_matcher is null || sourceFile is null) - return false; + public bool Exclude(string sourceFile) + { + if (_matcher is null || sourceFile is null) + return false; - // We strip out drive because it doesn't work with globbing - return _matcher.Match(Path.IsPathRooted(sourceFile) ? sourceFile.Substring(Path.GetPathRoot(sourceFile).Length) : sourceFile).HasMatches; - } + // We strip out drive because it doesn't work with globbing + return _matcher.Match(Path.IsPathRooted(sourceFile) ? sourceFile.Substring(Path.GetPathRoot(sourceFile).Length) : sourceFile).HasMatches; } + } } diff --git a/src/coverlet.core/Instrumentation/InstrumenterResult.cs b/src/coverlet.core/Instrumentation/InstrumenterResult.cs index f93b66866..d16e3c20b 100644 --- a/src/coverlet.core/Instrumentation/InstrumenterResult.cs +++ b/src/coverlet.core/Instrumentation/InstrumenterResult.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -9,115 +9,115 @@ namespace Coverlet.Core.Instrumentation { - [DebuggerDisplay("Number = {Number} Hits = {Hits} Class = {Class} Method = {Method}")] - [DataContract] - internal class Line - { - [DataMember] - public int Number; - [DataMember] - public string Class; - [DataMember] - public string Method; - [DataMember] - public int Hits; - } + [DebuggerDisplay("Number = {Number} Hits = {Hits} Class = {Class} Method = {Method}")] + [DataContract] + internal class Line + { + [DataMember] + public int Number; + [DataMember] + public string Class; + [DataMember] + public string Method; + [DataMember] + public int Hits; + } - [DebuggerDisplay("Line = {Number} Offset = {Offset} EndOffset = {EndOffset} Path = {Path} Ordinal = {Ordinal} Hits = {Hits}")] - [DataContract] - internal class Branch : Line - { - [DataMember] - public int Offset; - [DataMember] - public int EndOffset; - [DataMember] - public int Path; - [DataMember] - public uint Ordinal; - } + [DebuggerDisplay("Line = {Number} Offset = {Offset} EndOffset = {EndOffset} Path = {Path} Ordinal = {Ordinal} Hits = {Hits}")] + [DataContract] + internal class Branch : Line + { + [DataMember] + public int Offset; + [DataMember] + public int EndOffset; + [DataMember] + public int Path; + [DataMember] + public uint Ordinal; + } - [DebuggerDisplay("line = {Line} Ordinal = {Ordinal}")] - // Implements IEquatable because is used by dictionary key https://docs.microsoft.com/en-us/dotnet/api/system.iequatable-1?view=netcore-2.2#remarks - [DataContract] - internal class BranchKey : IEquatable - { - public BranchKey(int line, int ordinal) => (Line, Ordinal) = (line, ordinal); + [DebuggerDisplay("line = {Line} Ordinal = {Ordinal}")] + // Implements IEquatable because is used by dictionary key https://docs.microsoft.com/en-us/dotnet/api/system.iequatable-1?view=netcore-2.2#remarks + [DataContract] + internal class BranchKey : IEquatable + { + public BranchKey(int line, int ordinal) => (Line, Ordinal) = (line, ordinal); - [DataMember] - public int Line { get; set; } - [DataMember] - public int Ordinal { get; set; } + [DataMember] + public int Line { get; set; } + [DataMember] + public int Ordinal { get; set; } - public override bool Equals(object obj) => Equals(obj); + public override bool Equals(object obj) => Equals(obj); - public bool Equals(BranchKey other) => other is BranchKey branchKey && branchKey.Line == Line && branchKey.Ordinal == Ordinal; + public bool Equals(BranchKey other) => other is BranchKey branchKey && branchKey.Line == Line && branchKey.Ordinal == Ordinal; - public override int GetHashCode() - { - return (Line, Ordinal).GetHashCode(); - } + public override int GetHashCode() + { + return (Line, Ordinal).GetHashCode(); } + } - [DataContract] - internal class Document + [DataContract] + internal class Document + { + public Document() { - public Document() - { - Lines = new Dictionary(); - Branches = new Dictionary(); - } - - [DataMember] - public string Path; - [DataMember] - public int Index; - [DataMember] - public Dictionary Lines { get; private set; } - [DataMember] - public Dictionary Branches { get; private set; } + Lines = new Dictionary(); + Branches = new Dictionary(); } - [DebuggerDisplay("isBranch = {isBranch} docIndex = {docIndex} start = {start} end = {end}")] - [DataContract] - [SuppressMessage("Style", "IDE1006", Justification = "suppress casing error for API compatibility")] - internal class HitCandidate - { - public HitCandidate(bool isBranch, int docIndex, int start, int end) => (this.isBranch, this.docIndex, this.start, this.end) = (isBranch, docIndex, start, end); + [DataMember] + public string Path; + [DataMember] + public int Index; + [DataMember] + public Dictionary Lines { get; private set; } + [DataMember] + public Dictionary Branches { get; private set; } + } - [DataMember] - public bool isBranch { get; set; } - [DataMember] - public int docIndex { get; set; } - [DataMember] - public int start { get; set; } - [DataMember] - public int end { get; set; } - public HashSet AccountedByNestedInstrumentation { get; set; } - } + [DebuggerDisplay("isBranch = {isBranch} docIndex = {docIndex} start = {start} end = {end}")] + [DataContract] + [SuppressMessage("Style", "IDE1006", Justification = "suppress casing error for API compatibility")] + internal class HitCandidate + { + public HitCandidate(bool isBranch, int docIndex, int start, int end) => (this.isBranch, this.docIndex, this.start, this.end) = (isBranch, docIndex, start, end); - [DataContract] - internal class InstrumenterResult - { - public InstrumenterResult() - { - Documents = new Dictionary(); - HitCandidates = new List(); - } + [DataMember] + public bool isBranch { get; set; } + [DataMember] + public int docIndex { get; set; } + [DataMember] + public int start { get; set; } + [DataMember] + public int end { get; set; } + public HashSet AccountedByNestedInstrumentation { get; set; } + } - [DataMember] - public string Module; - [DataMember] - public string[] BranchesInCompiledGeneratedClass; - [DataMember] - public string HitsFilePath; - [DataMember] - public string ModulePath; - [DataMember] - public string SourceLink; - [DataMember] - public Dictionary Documents { get; private set; } - [DataMember] - public List HitCandidates { get; private set; } + [DataContract] + internal class InstrumenterResult + { + public InstrumenterResult() + { + Documents = new Dictionary(); + HitCandidates = new List(); } + + [DataMember] + public string Module; + [DataMember] + public string[] BranchesInCompiledGeneratedClass; + [DataMember] + public string HitsFilePath; + [DataMember] + public string ModulePath; + [DataMember] + public string SourceLink; + [DataMember] + public Dictionary Documents { get; private set; } + [DataMember] + public List HitCandidates { get; private set; } + } } diff --git a/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs index 600fe91b1..71b2b5664 100644 --- a/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs +++ b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs @@ -10,192 +10,192 @@ namespace Coverlet.Core.Instrumentation { - /// - /// This static class will be injected on a module being instrumented in order to direct on module hits - /// to a single location. - /// - /// - /// As this type is going to be customized for each instrumented module it doesn't follow typical practices - /// regarding visibility of members, etc. - /// - [CompilerGenerated] - [ExcludeFromCodeCoverage] - internal static class ModuleTrackerTemplate + /// + /// This static class will be injected on a module being instrumented in order to direct on module hits + /// to a single location. + /// + /// + /// As this type is going to be customized for each instrumented module it doesn't follow typical practices + /// regarding visibility of members, etc. + /// + [CompilerGenerated] + [ExcludeFromCodeCoverage] + internal static class ModuleTrackerTemplate + { + public static string HitsFilePath; + public static int[] HitsArray; + public static bool SingleHit; + public static bool FlushHitFile; + private static readonly bool s_enableLog = int.TryParse(Environment.GetEnvironmentVariable("COVERLET_ENABLETRACKERLOG"), out int result) && result == 1; + private static readonly string s_sessionId = Guid.NewGuid().ToString(); + + static ModuleTrackerTemplate() { - public static string HitsFilePath; - public static int[] HitsArray; - public static bool SingleHit; - public static bool FlushHitFile; - private static readonly bool s_enableLog = int.TryParse(Environment.GetEnvironmentVariable("COVERLET_ENABLETRACKERLOG"), out int result) && result == 1; - private static readonly string s_sessionId = Guid.NewGuid().ToString(); - - static ModuleTrackerTemplate() - { - // At the end of the instrumentation of a module, the instrumenter needs to add code here - // to initialize the static fields according to the values derived from the instrumentation of - // the module. - } + // At the end of the instrumentation of a module, the instrumenter needs to add code here + // to initialize the static fields according to the values derived from the instrumentation of + // the module. + } - // A call to this method will be injected in the static constructor above for most cases. However, if the - // current assembly is System.Private.CoreLib (or more specifically, defines System.AppDomain), a call directly - // to UnloadModule will be injected in System.AppContext.OnProcessExit. - public static void RegisterUnloadEvents() - { - AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule); - AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule); - } + // A call to this method will be injected in the static constructor above for most cases. However, if the + // current assembly is System.Private.CoreLib (or more specifically, defines System.AppDomain), a call directly + // to UnloadModule will be injected in System.AppContext.OnProcessExit. + public static void RegisterUnloadEvents() + { + AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule); + AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule); + } - public static void RecordHitInCoreLibrary(int hitLocationIndex) - { - // Make sure to avoid recording if this is a call to RecordHit within the AppDomain setup code in an - // instrumented build of System.Private.CoreLib. - if (HitsArray is null) - return; + public static void RecordHitInCoreLibrary(int hitLocationIndex) + { + // Make sure to avoid recording if this is a call to RecordHit within the AppDomain setup code in an + // instrumented build of System.Private.CoreLib. + if (HitsArray is null) + return; - Interlocked.Increment(ref HitsArray[hitLocationIndex]); - } + Interlocked.Increment(ref HitsArray[hitLocationIndex]); + } - public static void RecordHit(int hitLocationIndex) - { - Interlocked.Increment(ref HitsArray[hitLocationIndex]); - } + public static void RecordHit(int hitLocationIndex) + { + Interlocked.Increment(ref HitsArray[hitLocationIndex]); + } - public static void RecordSingleHitInCoreLibrary(int hitLocationIndex) - { - // Make sure to avoid recording if this is a call to RecordHit within the AppDomain setup code in an - // instrumented build of System.Private.CoreLib. - if (HitsArray is null) - return; - - ref int location = ref HitsArray[hitLocationIndex]; - if (location == 0) - location = 1; - } + public static void RecordSingleHitInCoreLibrary(int hitLocationIndex) + { + // Make sure to avoid recording if this is a call to RecordHit within the AppDomain setup code in an + // instrumented build of System.Private.CoreLib. + if (HitsArray is null) + return; + + ref int location = ref HitsArray[hitLocationIndex]; + if (location == 0) + location = 1; + } - public static void RecordSingleHit(int hitLocationIndex) - { - ref int location = ref HitsArray[hitLocationIndex]; - if (location == 0) - location = 1; - } + public static void RecordSingleHit(int hitLocationIndex) + { + ref int location = ref HitsArray[hitLocationIndex]; + if (location == 0) + location = 1; + } - public static void UnloadModule(object sender, EventArgs e) + public static void UnloadModule(object sender, EventArgs e) + { + // The same module can be unloaded multiple times in the same process via different app domains. + // Use a global mutex to ensure no concurrent access. + using var mutex = new Mutex(true, Path.GetFileNameWithoutExtension(HitsFilePath) + "_Mutex", out bool createdNew); + if (!createdNew) + { + mutex.WaitOne(); + } + + if (FlushHitFile) + { + try { - // The same module can be unloaded multiple times in the same process via different app domains. - // Use a global mutex to ensure no concurrent access. - using var mutex = new Mutex(true, Path.GetFileNameWithoutExtension(HitsFilePath) + "_Mutex", out bool createdNew); - if (!createdNew) + // Claim the current hits array and reset it to prevent double-counting scenarios. + int[] hitsArray = Interlocked.Exchange(ref HitsArray, new int[HitsArray.Length]); + + WriteLog($"Unload called for '{Assembly.GetExecutingAssembly().Location}' by '{sender ?? "null"}'"); + WriteLog($"Flushing hit file '{HitsFilePath}'"); + + bool failedToCreateNewHitsFile = false; + try + { + using var fs = new FileStream(HitsFilePath, FileMode.CreateNew); + using var bw = new BinaryWriter(fs); + bw.Write(hitsArray.Length); + foreach (int hitCount in hitsArray) + { + bw.Write(hitCount); + } + } + catch (Exception ex) + { + WriteLog($"Failed to create new hits file '{HitsFilePath}' -> '{ex.Message}'"); + failedToCreateNewHitsFile = true; + } + + if (failedToCreateNewHitsFile) + { + // Update the number of hits by adding value on disk with the ones on memory. + // This path should be triggered only in the case of multiple AppDomain unloads. + using var fs = new FileStream(HitsFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + using var br = new BinaryReader(fs); + using var bw = new BinaryWriter(fs); + int hitsLength = br.ReadInt32(); + WriteLog($"Current hits found '{hitsLength}'"); + + if (hitsLength != hitsArray.Length) { - mutex.WaitOne(); + throw new InvalidOperationException($"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {hitsArray.Length}"); } - if (FlushHitFile) + for (int i = 0; i < hitsLength; ++i) { - try - { - // Claim the current hits array and reset it to prevent double-counting scenarios. - int[] hitsArray = Interlocked.Exchange(ref HitsArray, new int[HitsArray.Length]); - - WriteLog($"Unload called for '{Assembly.GetExecutingAssembly().Location}' by '{sender ?? "null"}'"); - WriteLog($"Flushing hit file '{HitsFilePath}'"); - - bool failedToCreateNewHitsFile = false; - try - { - using var fs = new FileStream(HitsFilePath, FileMode.CreateNew); - using var bw = new BinaryWriter(fs); - bw.Write(hitsArray.Length); - foreach (int hitCount in hitsArray) - { - bw.Write(hitCount); - } - } - catch (Exception ex) - { - WriteLog($"Failed to create new hits file '{HitsFilePath}' -> '{ex.Message}'"); - failedToCreateNewHitsFile = true; - } - - if (failedToCreateNewHitsFile) - { - // Update the number of hits by adding value on disk with the ones on memory. - // This path should be triggered only in the case of multiple AppDomain unloads. - using var fs = new FileStream(HitsFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); - using var br = new BinaryReader(fs); - using var bw = new BinaryWriter(fs); - int hitsLength = br.ReadInt32(); - WriteLog($"Current hits found '{hitsLength}'"); - - if (hitsLength != hitsArray.Length) - { - throw new InvalidOperationException($"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {hitsArray.Length}"); - } - - for (int i = 0; i < hitsLength; ++i) - { - int oldHitCount = br.ReadInt32(); - bw.Seek(-sizeof(int), SeekOrigin.Current); - if (SingleHit) - { - bw.Write(hitsArray[i] + oldHitCount > 0 ? 1 : 0); - } - else - { - bw.Write(hitsArray[i] + oldHitCount); - } - } - } - - WriteHits(sender); - - WriteLog($"Hit file '{HitsFilePath}' flushed, size {new FileInfo(HitsFilePath).Length}"); - WriteLog("--------------------------------"); - } - catch (Exception ex) - { - WriteLog(ex.ToString()); - throw; - } + int oldHitCount = br.ReadInt32(); + bw.Seek(-sizeof(int), SeekOrigin.Current); + if (SingleHit) + { + bw.Write(hitsArray[i] + oldHitCount > 0 ? 1 : 0); + } + else + { + bw.Write(hitsArray[i] + oldHitCount); + } } + } - // On purpose this is not under a try-finally: it is better to have an exception if there was any error writing the hits file - // this case is relevant when instrumenting corelib since multiple processes can be running against the same instrumented dll. - mutex.ReleaseMutex(); - } + WriteHits(sender); - private static void WriteHits(object sender) + WriteLog($"Hit file '{HitsFilePath}' flushed, size {new FileInfo(HitsFilePath).Length}"); + WriteLog("--------------------------------"); + } + catch (Exception ex) { - if (s_enableLog) - { - var currentAssembly = Assembly.GetExecutingAssembly(); - var location = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(currentAssembly.Location), "TrackersHitsLog")); - location.Create(); - string logFile = Path.Combine(location.FullName, $"{Path.GetFileName(currentAssembly.Location)}_{DateTime.UtcNow.Ticks}_{s_sessionId}.txt"); - using (var fs = new FileStream(HitsFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) - using (var log = new FileStream(logFile, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None)) - using (var logWriter = new StreamWriter(log)) - using (var br = new BinaryReader(fs)) - { - int hitsLength = br.ReadInt32(); - for (int i = 0; i < hitsLength; ++i) - { - logWriter.WriteLine($"{i},{br.ReadInt32()}"); - } - } - - File.AppendAllText(logFile, $"Hits flushed file path {HitsFilePath} location '{Assembly.GetExecutingAssembly().Location}' by '{sender ?? "null"}'"); - } + WriteLog(ex.ToString()); + throw; } + } - private static void WriteLog(string logText) + // On purpose this is not under a try-finally: it is better to have an exception if there was any error writing the hits file + // this case is relevant when instrumenting corelib since multiple processes can be running against the same instrumented dll. + mutex.ReleaseMutex(); + } + + private static void WriteHits(object sender) + { + if (s_enableLog) + { + var currentAssembly = Assembly.GetExecutingAssembly(); + var location = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(currentAssembly.Location), "TrackersHitsLog")); + location.Create(); + string logFile = Path.Combine(location.FullName, $"{Path.GetFileName(currentAssembly.Location)}_{DateTime.UtcNow.Ticks}_{s_sessionId}.txt"); + using (var fs = new FileStream(HitsFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) + using (var log = new FileStream(logFile, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None)) + using (var logWriter = new StreamWriter(log)) + using (var br = new BinaryReader(fs)) { - if (s_enableLog) - { - // We don't set path as global var to keep benign possible errors inside try/catch - // I'm not sure that location will be ok in every scenario - string location = Assembly.GetExecutingAssembly().Location; - File.AppendAllText(Path.Combine(Path.GetDirectoryName(location), Path.GetFileName(location) + "_tracker.txt"), $"[{DateTime.UtcNow} S:{s_sessionId} T:{Thread.CurrentThread.ManagedThreadId}]{logText}{Environment.NewLine}"); - } + int hitsLength = br.ReadInt32(); + for (int i = 0; i < hitsLength; ++i) + { + logWriter.WriteLine($"{i},{br.ReadInt32()}"); + } } + + File.AppendAllText(logFile, $"Hits flushed file path {HitsFilePath} location '{Assembly.GetExecutingAssembly().Location}' by '{sender ?? "null"}'"); + } + } + + private static void WriteLog(string logText) + { + if (s_enableLog) + { + // We don't set path as global var to keep benign possible errors inside try/catch + // I'm not sure that location will be ok in every scenario + string location = Assembly.GetExecutingAssembly().Location; + File.AppendAllText(Path.Combine(Path.GetDirectoryName(location), Path.GetFileName(location) + "_tracker.txt"), $"[{DateTime.UtcNow} S:{s_sessionId} T:{Thread.CurrentThread.ManagedThreadId}]{logText}{Environment.NewLine}"); + } } + } } diff --git a/src/coverlet.core/Instrumentation/ReachabilityHelper.cs b/src/coverlet.core/Instrumentation/ReachabilityHelper.cs index 1b4840d85..f61016fec 100644 --- a/src/coverlet.core/Instrumentation/ReachabilityHelper.cs +++ b/src/coverlet.core/Instrumentation/ReachabilityHelper.cs @@ -11,168 +11,168 @@ namespace Coverlet.Core.Instrumentation.Reachability { + /// + /// Helper for find unreachable IL instructions. + /// + internal class ReachabilityHelper + { + internal readonly struct UnreachableRange + { + /// + /// Offset of first unreachable instruction. + /// + public int StartOffset { get; } + /// + /// Offset of last unreachable instruction. + /// + public int EndOffset { get; } + + public UnreachableRange(int start, int end) + { + StartOffset = start; + EndOffset = end; + } + + public override string ToString() + => $"[IL_{StartOffset:x4}, IL_{EndOffset:x4}]"; + } + + private class BasicBlock + { + /// + /// Offset of the instruction that starts the block + /// + public int StartOffset { get; } + /// + /// Whether it is possible for control to flow past the end of the block, + /// ie. whether it's tail is reachable + /// + public bool TailReachable => UnreachableAfter == null; + /// + /// If control flows to the end of the block, where it can flow to + /// + public ImmutableArray BranchesTo { get; } + + /// + /// If this block contains a call(i) to a method that does not return + /// this will be the first such call. + /// + public Instruction UnreachableAfter { get; } + + /// + /// If an exception is raised in this block, where control might branch to. + /// + /// Note that this can happen even if the block's end is unreachable. + /// + public ImmutableArray ExceptionBranchesTo { get; } + + /// + /// Mutable, records whether control can flow into the block, + /// ie. whether it's head is reachable + /// + public bool HeadReachable { get; set; } + + public BasicBlock(int startOffset, Instruction unreachableAfter, ImmutableArray branchesTo, ImmutableArray exceptionBranchesTo) + { + StartOffset = startOffset; + UnreachableAfter = unreachableAfter; + BranchesTo = branchesTo; + ExceptionBranchesTo = exceptionBranchesTo; + } + + public override string ToString() + => $"{nameof(StartOffset)}=IL_{StartOffset:x4}, {nameof(HeadReachable)}={HeadReachable}, {nameof(TailReachable)}={TailReachable}, {nameof(BranchesTo)}=({string.Join(", ", BranchesTo.Select(b => $"IL_{b:x4}"))}), {nameof(ExceptionBranchesTo)}=({string.Join(", ", ExceptionBranchesTo.Select(b => $"IL_{b:x4}"))}), {nameof(UnreachableAfter)}={(UnreachableAfter != null ? $"IL_{UnreachableAfter:x4}" : "")}"; + } + /// - /// Helper for find unreachable IL instructions. + /// Represents an Instruction that transitions control flow (ie. branches). + /// + /// This is _different_ from other branch types, like Branch and BranchPoint + /// because it includes unconditional branches too. /// - internal class ReachabilityHelper + private readonly struct BranchInstruction { - internal readonly struct UnreachableRange + /// + /// Location of the branching instruction + /// + public int Offset { get; } + + /// + /// Returns true if this branch has multiple targets. + /// + public bool HasMultiTargets => _targetOffset == -1; + + private readonly int _targetOffset; + + /// + /// Target of the branch, assuming it has a single target. + /// + /// It is illegal to access this if there are multiple targets. + /// + public int TargetOffset + { + get { - /// - /// Offset of first unreachable instruction. - /// - public int StartOffset { get; } - /// - /// Offset of last unreachable instruction. - /// - public int EndOffset { get; } - - public UnreachableRange(int start, int end) - { - StartOffset = start; - EndOffset = end; - } + if (HasMultiTargets) + { + throw new InvalidOperationException($"{HasMultiTargets} is true"); + } - public override string ToString() - => $"[IL_{StartOffset:x4}, IL_{EndOffset:x4}]"; + return _targetOffset; } - - private class BasicBlock + } + + private readonly ImmutableArray _targetOffsets; + + /// + /// Targets of the branch, assuming it has multiple targets. + /// + /// It is illegal to access this if there is a single target. + /// + public ImmutableArray TargetOffsets + { + get { - /// - /// Offset of the instruction that starts the block - /// - public int StartOffset { get; } - /// - /// Whether it is possible for control to flow past the end of the block, - /// ie. whether it's tail is reachable - /// - public bool TailReachable => UnreachableAfter == null; - /// - /// If control flows to the end of the block, where it can flow to - /// - public ImmutableArray BranchesTo { get; } - - /// - /// If this block contains a call(i) to a method that does not return - /// this will be the first such call. - /// - public Instruction UnreachableAfter { get; } - - /// - /// If an exception is raised in this block, where control might branch to. - /// - /// Note that this can happen even if the block's end is unreachable. - /// - public ImmutableArray ExceptionBranchesTo { get; } - - /// - /// Mutable, records whether control can flow into the block, - /// ie. whether it's head is reachable - /// - public bool HeadReachable { get; set; } - - public BasicBlock(int startOffset, Instruction unreachableAfter, ImmutableArray branchesTo, ImmutableArray exceptionBranchesTo) - { - StartOffset = startOffset; - UnreachableAfter = unreachableAfter; - BranchesTo = branchesTo; - ExceptionBranchesTo = exceptionBranchesTo; - } + if (!HasMultiTargets) + { + throw new InvalidOperationException($"{HasMultiTargets} is false"); + } - public override string ToString() - => $"{nameof(StartOffset)}=IL_{StartOffset:x4}, {nameof(HeadReachable)}={HeadReachable}, {nameof(TailReachable)}={TailReachable}, {nameof(BranchesTo)}=({string.Join(", ", BranchesTo.Select(b => $"IL_{b:x4}"))}), {nameof(ExceptionBranchesTo)}=({string.Join(", ", ExceptionBranchesTo.Select(b => $"IL_{b:x4}"))}), {nameof(UnreachableAfter)}={(UnreachableAfter != null ? $"IL_{UnreachableAfter:x4}" : "")}"; + return _targetOffsets; } - - /// - /// Represents an Instruction that transitions control flow (ie. branches). - /// - /// This is _different_ from other branch types, like Branch and BranchPoint - /// because it includes unconditional branches too. - /// - private readonly struct BranchInstruction + } + + public BranchInstruction(int offset, int targetOffset) + { + Offset = offset; + _targetOffset = targetOffset; + _targetOffsets = ImmutableArray.Empty; + } + + public BranchInstruction(int offset, ImmutableArray targetOffset) + { + if (targetOffset.Length == 1) { - /// - /// Location of the branching instruction - /// - public int Offset { get; } - - /// - /// Returns true if this branch has multiple targets. - /// - public bool HasMultiTargets => _targetOffset == -1; - - private readonly int _targetOffset; - - /// - /// Target of the branch, assuming it has a single target. - /// - /// It is illegal to access this if there are multiple targets. - /// - public int TargetOffset - { - get - { - if (HasMultiTargets) - { - throw new InvalidOperationException($"{HasMultiTargets} is true"); - } - - return _targetOffset; - } - } + throw new ArgumentException("Use single entry constructor for single targets", nameof(targetOffset)); + } - private readonly ImmutableArray _targetOffsets; + Offset = offset; + _targetOffset = -1; + _targetOffsets = targetOffset; + } - /// - /// Targets of the branch, assuming it has multiple targets. - /// - /// It is illegal to access this if there is a single target. - /// - public ImmutableArray TargetOffsets - { - get - { - if (!HasMultiTargets) - { - throw new InvalidOperationException($"{HasMultiTargets} is false"); - } - - return _targetOffsets; - } - } - - public BranchInstruction(int offset, int targetOffset) - { - Offset = offset; - _targetOffset = targetOffset; - _targetOffsets = ImmutableArray.Empty; - } + public override string ToString() + => $"IL_{Offset:x4}: {(HasMultiTargets ? string.Join(", ", TargetOffsets.Select(x => $"IL_{x:x4}")) : $"IL_{TargetOffset:x4}")}"; + } - public BranchInstruction(int offset, ImmutableArray targetOffset) + /// + /// OpCodes that transfer control code, even if they do not + /// introduce branch points. + /// + private static readonly ImmutableHashSet s_branchOpCodes = + ImmutableHashSet.CreateRange( + new[] { - if (targetOffset.Length == 1) - { - throw new ArgumentException("Use single entry constructor for single targets", nameof(targetOffset)); - } - - Offset = offset; - _targetOffset = -1; - _targetOffsets = targetOffset; - } - - public override string ToString() - => $"IL_{Offset:x4}: {(HasMultiTargets ? string.Join(", ", TargetOffsets.Select(x => $"IL_{x:x4}")) : $"IL_{TargetOffset:x4}")}"; - } - - /// - /// OpCodes that transfer control code, even if they do not - /// introduce branch points. - /// - private static readonly ImmutableHashSet s_branchOpCodes = - ImmutableHashSet.CreateRange( - new[] - { OpCodes.Beq, OpCodes.Beq_S, OpCodes.Bge, @@ -217,600 +217,600 @@ public override string ToString() // look at exception handlers to figure out where they go to OpCodes.Endfilter, OpCodes.Endfinally - } - ); - - /// - /// OpCodes that unconditionally transfer control, so there - /// is not "fall through" branch target. - /// - private static readonly ImmutableHashSet s_unconditionalBranchOpCodes = - ImmutableHashSet.CreateRange( - new[] - { + } + ); + + /// + /// OpCodes that unconditionally transfer control, so there + /// is not "fall through" branch target. + /// + private static readonly ImmutableHashSet s_unconditionalBranchOpCodes = + ImmutableHashSet.CreateRange( + new[] + { OpCodes.Br, OpCodes.Br_S, OpCodes.Leave, OpCodes.Leave_S - } - ); + } + ); - private readonly ImmutableHashSet _doesNotReturnMethods; + private readonly ImmutableHashSet _doesNotReturnMethods; - private ReachabilityHelper(ImmutableHashSet doesNotReturnMethods) - { - _doesNotReturnMethods = doesNotReturnMethods; - } + private ReachabilityHelper(ImmutableHashSet doesNotReturnMethods) + { + _doesNotReturnMethods = doesNotReturnMethods; + } - /// - /// Build a ReachabilityHelper for the given module. - /// - /// Predetermines methods that will not return, as - /// indicated by the presense of the given attributes. - /// - public static ReachabilityHelper CreateForModule(ModuleDefinition module, string[] doesNotReturnAttributes, ILogger logger) + /// + /// Build a ReachabilityHelper for the given module. + /// + /// Predetermines methods that will not return, as + /// indicated by the presense of the given attributes. + /// + public static ReachabilityHelper CreateForModule(ModuleDefinition module, string[] doesNotReturnAttributes, ILogger logger) + { + if (doesNotReturnAttributes.Length == 0) + { + return new ReachabilityHelper(ImmutableHashSet.Empty); + } + + ImmutableHashSet processedMethods = ImmutableHashSet.Empty; + ImmutableHashSet.Builder doNotReturn = ImmutableHashSet.CreateBuilder(); + foreach (TypeDefinition type in module.Types) + { + foreach (MethodDefinition mtd in type.Methods) { - if (doesNotReturnAttributes.Length == 0) + if (mtd.IsNative) + { + continue; + } + + MethodBody body; + try + { + if (!mtd.HasBody) { - return new ReachabilityHelper(ImmutableHashSet.Empty); + continue; } - ImmutableHashSet processedMethods = ImmutableHashSet.Empty; - ImmutableHashSet.Builder doNotReturn = ImmutableHashSet.CreateBuilder(); - foreach (TypeDefinition type in module.Types) + body = mtd.Body; + } + catch + { + continue; + } + + foreach (Instruction instr in body.Instructions) + { + if (!IsCall(instr, out MethodReference calledMtd)) { - foreach (MethodDefinition mtd in type.Methods) - { - if (mtd.IsNative) - { - continue; - } - - MethodBody body; - try - { - if (!mtd.HasBody) - { - continue; - } - - body = mtd.Body; - } - catch - { - continue; - } - - foreach (Instruction instr in body.Instructions) - { - if (!IsCall(instr, out MethodReference calledMtd)) - { - continue; - } - - MetadataToken token = calledMtd.MetadataToken; - if (processedMethods.Contains(token)) - { - continue; - } - - processedMethods = processedMethods.Add(token); - - MethodDefinition mtdDef; - try - { - mtdDef = calledMtd.Resolve(); - } - catch - { - logger.LogWarning($"Unable to resolve method reference \"{calledMtd.FullName}\", assuming calls to will return"); - mtdDef = null; - } - - if (mtdDef == null) - { - continue; - } - - if (!mtdDef.HasCustomAttributes) - { - continue; - } - - bool hasDoesNotReturnAttribute = false; - foreach (CustomAttribute attr in mtdDef.CustomAttributes) - { - if (Array.IndexOf(doesNotReturnAttributes, attr.AttributeType.Name) != -1) - { - hasDoesNotReturnAttribute = true; - break; - } - } - - if (hasDoesNotReturnAttribute) - { - logger.LogVerbose($"Determined call to \"{calledMtd.FullName}\" will not return"); - doNotReturn.Add(token); - } - } - } + continue; } - ImmutableHashSet doNoReturnTokens = doNotReturn.ToImmutable(); + MetadataToken token = calledMtd.MetadataToken; + if (processedMethods.Contains(token)) + { + continue; + } - return new ReachabilityHelper(doNoReturnTokens); - } + processedMethods = processedMethods.Add(token); - /// - /// Calculates which IL instructions are reachable given an instruction stream and branch points extracted from a method. - /// - /// The algorithm works like so: - /// 1. determine the "blocks" that make up a function - /// * A block starts with either the start of the method, or a branch _target_ - /// * blocks are "where some other code might jump to" - /// * blocks end with either another branch, another branch target, or the end of the method - /// * this means blocks contain no control flow, except (maybe) as the very last instruction - /// 2. blocks have head and tail reachability - /// * a block has head reachablility if some other block that is reachable can branch to it - /// * a block has tail reachability if it contains no calls to methods that never return - /// 4. push the first block onto a stack - /// 5. while the stack is not empty - /// a. pop a block off the stack - /// b. give it head reachability - /// c. if the pop'd block is tail reachable, push the blocks it can branch to onto the stack - /// 6. consider each block - /// * if it is head and tail reachable, all instructions in it are reachable - /// * if it is not head reachable (regardless of tail reachability), no instructions in it are reachable - /// * if it is only head reachable, all instructions up to and including the first call to a method that does not return are reachable - /// - public ImmutableArray FindUnreachableIL(Collection instrs, Collection exceptionHandlers) - { - // no instructions, means nothing to... not reach - if (instrs.Count == 0) + MethodDefinition mtdDef; + try + { + mtdDef = calledMtd.Resolve(); + } + catch { - return ImmutableArray.Empty; + logger.LogWarning($"Unable to resolve method reference \"{calledMtd.FullName}\", assuming calls to will return"); + mtdDef = null; } - // no known methods that do not return, so everything is reachable by definition - if (_doesNotReturnMethods.IsEmpty) + if (mtdDef == null) { - return ImmutableArray.Empty; + continue; } - (bool mayContainUnreachableCode, ImmutableArray branches) = AnalyzeInstructions(instrs, exceptionHandlers); + if (!mtdDef.HasCustomAttributes) + { + continue; + } - // no need to do any more work, nothing unreachable here - if (!mayContainUnreachableCode) + bool hasDoesNotReturnAttribute = false; + foreach (CustomAttribute attr in mtdDef.CustomAttributes) { - return ImmutableArray.Empty; + if (Array.IndexOf(doesNotReturnAttributes, attr.AttributeType.Name) != -1) + { + hasDoesNotReturnAttribute = true; + break; + } } - Instruction lastInstr = instrs[instrs.Count - 1]; + if (hasDoesNotReturnAttribute) + { + logger.LogVerbose($"Determined call to \"{calledMtd.FullName}\" will not return"); + doNotReturn.Add(token); + } + } + } + } + + ImmutableHashSet doNoReturnTokens = doNotReturn.ToImmutable(); + + return new ReachabilityHelper(doNoReturnTokens); + } + + /// + /// Calculates which IL instructions are reachable given an instruction stream and branch points extracted from a method. + /// + /// The algorithm works like so: + /// 1. determine the "blocks" that make up a function + /// * A block starts with either the start of the method, or a branch _target_ + /// * blocks are "where some other code might jump to" + /// * blocks end with either another branch, another branch target, or the end of the method + /// * this means blocks contain no control flow, except (maybe) as the very last instruction + /// 2. blocks have head and tail reachability + /// * a block has head reachablility if some other block that is reachable can branch to it + /// * a block has tail reachability if it contains no calls to methods that never return + /// 4. push the first block onto a stack + /// 5. while the stack is not empty + /// a. pop a block off the stack + /// b. give it head reachability + /// c. if the pop'd block is tail reachable, push the blocks it can branch to onto the stack + /// 6. consider each block + /// * if it is head and tail reachable, all instructions in it are reachable + /// * if it is not head reachable (regardless of tail reachability), no instructions in it are reachable + /// * if it is only head reachable, all instructions up to and including the first call to a method that does not return are reachable + /// + public ImmutableArray FindUnreachableIL(Collection instrs, Collection exceptionHandlers) + { + // no instructions, means nothing to... not reach + if (instrs.Count == 0) + { + return ImmutableArray.Empty; + } + + // no known methods that do not return, so everything is reachable by definition + if (_doesNotReturnMethods.IsEmpty) + { + return ImmutableArray.Empty; + } + + (bool mayContainUnreachableCode, ImmutableArray branches) = AnalyzeInstructions(instrs, exceptionHandlers); + + // no need to do any more work, nothing unreachable here + if (!mayContainUnreachableCode) + { + return ImmutableArray.Empty; + } + + Instruction lastInstr = instrs[instrs.Count - 1]; + + ImmutableArray blocks = CreateBasicBlocks(instrs, exceptionHandlers, branches); - ImmutableArray blocks = CreateBasicBlocks(instrs, exceptionHandlers, branches); + DetermineHeadReachability(blocks); + return DetermineUnreachableRanges(blocks, lastInstr.Offset); + } + + /// + /// Analyzes the instructiona and exception handlers provided to find branches and determine if + /// it is possible for their to be unreachable code. + /// + private (bool MayContainUnreachableCode, ImmutableArray Branches) AnalyzeInstructions(Collection instrs, Collection exceptionHandlers) + { + bool containsDoesNotReturnCall = false; + + ImmutableArray.Builder ret = ImmutableArray.CreateBuilder(); + foreach (Instruction i in instrs) + { + containsDoesNotReturnCall = containsDoesNotReturnCall || DoesNotReturn(i); - DetermineHeadReachability(blocks); - return DetermineUnreachableRanges(blocks, lastInstr.Offset); + if (s_branchOpCodes.Contains(i.OpCode)) + { + (int? singleTargetOffset, ImmutableArray multiTargetOffsets) = GetInstructionTargets(i, exceptionHandlers); + + if (singleTargetOffset != null) + { + ret.Add(new BranchInstruction(i.Offset, singleTargetOffset.Value)); + } + else + { + ret.Add(new BranchInstruction(i.Offset, multiTargetOffsets)); + } } + } + + return (containsDoesNotReturnCall, ret.ToImmutable()); + } - /// - /// Analyzes the instructiona and exception handlers provided to find branches and determine if - /// it is possible for their to be unreachable code. - /// - private (bool MayContainUnreachableCode, ImmutableArray Branches) AnalyzeInstructions(Collection instrs, Collection exceptionHandlers) + /// + /// For a single instruction, determines all the places it might branch to. + /// + private static (int? SingleTargetOffset, ImmutableArray MultiTargetOffsets) GetInstructionTargets(Instruction i, Collection exceptionHandlers) + { + int? singleTargetOffset; + ImmutableArray multiTargetOffsets; + + if (i.Operand is Instruction[] multiTarget) + { + // it's a switch + singleTargetOffset = null; + + multiTargetOffsets = ImmutableArray.Create(i.Next.Offset); + foreach (Instruction instr in multiTarget) { - bool containsDoesNotReturnCall = false; + // in practice these are small arrays, so a scan should be fine + if (multiTargetOffsets.Contains(instr.Offset)) + { + continue; + } - ImmutableArray.Builder ret = ImmutableArray.CreateBuilder(); - foreach (Instruction i in instrs) - { - containsDoesNotReturnCall = containsDoesNotReturnCall || DoesNotReturn(i); - - if (s_branchOpCodes.Contains(i.OpCode)) - { - (int? singleTargetOffset, ImmutableArray multiTargetOffsets) = GetInstructionTargets(i, exceptionHandlers); - - if (singleTargetOffset != null) - { - ret.Add(new BranchInstruction(i.Offset, singleTargetOffset.Value)); - } - else - { - ret.Add(new BranchInstruction(i.Offset, multiTargetOffsets)); - } - } - } + multiTargetOffsets = multiTargetOffsets.Add(instr.Offset); + } + } + else if (i.Operand is Instruction targetInstr) + { + // it's any of the B.*(_S)? or Leave(_S)? instructions - return (containsDoesNotReturnCall, ret.ToImmutable()); + if (s_unconditionalBranchOpCodes.Contains(i.OpCode)) + { + multiTargetOffsets = ImmutableArray.Empty; + singleTargetOffset = targetInstr.Offset; + } + else + { + singleTargetOffset = null; + multiTargetOffsets = ImmutableArray.Create(i.Next.Offset, targetInstr.Offset); + } + } + else if (i.OpCode == OpCodes.Endfilter) + { + // Endfilter is always the last instruction in a filter block, and no sort of control + // flow is allowed so we can scan backwards to see find the block + + ExceptionHandler filterForHandler = null; + foreach (ExceptionHandler handler in exceptionHandlers) + { + if (handler.FilterStart == null) + { + continue; + } + + Instruction startsAt = handler.FilterStart; + Instruction cur = startsAt; + while (cur != null && cur.Offset < i.Offset) + { + cur = cur.Next; + } + + if (cur != null && cur.Offset == i.Offset) + { + filterForHandler = handler; + break; + } } - /// - /// For a single instruction, determines all the places it might branch to. - /// - private static (int? SingleTargetOffset, ImmutableArray MultiTargetOffsets) GetInstructionTargets(Instruction i, Collection exceptionHandlers) + if (filterForHandler == null) { - int? singleTargetOffset; - ImmutableArray multiTargetOffsets; + throw new InvalidOperationException($"Could not find ExceptionHandler associated with {i}"); + } - if (i.Operand is Instruction[] multiTarget) - { - // it's a switch - singleTargetOffset = null; - - multiTargetOffsets = ImmutableArray.Create(i.Next.Offset); - foreach (Instruction instr in multiTarget) - { - // in practice these are small arrays, so a scan should be fine - if (multiTargetOffsets.Contains(instr.Offset)) - { - continue; - } - - multiTargetOffsets = multiTargetOffsets.Add(instr.Offset); - } - } - else if (i.Operand is Instruction targetInstr) - { - // it's any of the B.*(_S)? or Leave(_S)? instructions - - if (s_unconditionalBranchOpCodes.Contains(i.OpCode)) - { - multiTargetOffsets = ImmutableArray.Empty; - singleTargetOffset = targetInstr.Offset; - } - else - { - singleTargetOffset = null; - multiTargetOffsets = ImmutableArray.Create(i.Next.Offset, targetInstr.Offset); - } - } - else if (i.OpCode == OpCodes.Endfilter) - { - // Endfilter is always the last instruction in a filter block, and no sort of control - // flow is allowed so we can scan backwards to see find the block - - ExceptionHandler filterForHandler = null; - foreach (ExceptionHandler handler in exceptionHandlers) - { - if (handler.FilterStart == null) - { - continue; - } - - Instruction startsAt = handler.FilterStart; - Instruction cur = startsAt; - while (cur != null && cur.Offset < i.Offset) - { - cur = cur.Next; - } - - if (cur != null && cur.Offset == i.Offset) - { - filterForHandler = handler; - break; - } - } - - if (filterForHandler == null) - { - throw new InvalidOperationException($"Could not find ExceptionHandler associated with {i}"); - } - - // filter can do one of two things: - // - branch into handler - // - percolate to another catch block, which might not be in this method - // - // so we chose to model this as an unconditional branch into the handler - singleTargetOffset = filterForHandler.HandlerStart.Offset; - multiTargetOffsets = ImmutableArray.Empty; - } - else if (i.OpCode == OpCodes.Endfinally) - { - // Endfinally is very weird - // - // what it does, effectively is "take whatever branch would normally happen after the instruction - // that left the paired try - // - // practically, this makes endfinally a branch with no target - - singleTargetOffset = null; - multiTargetOffsets = ImmutableArray.Empty; - } - else - { - throw new InvalidOperationException($"Unexpected operand when processing branch {i}"); - } + // filter can do one of two things: + // - branch into handler + // - percolate to another catch block, which might not be in this method + // + // so we chose to model this as an unconditional branch into the handler + singleTargetOffset = filterForHandler.HandlerStart.Offset; + multiTargetOffsets = ImmutableArray.Empty; + } + else if (i.OpCode == OpCodes.Endfinally) + { + // Endfinally is very weird + // + // what it does, effectively is "take whatever branch would normally happen after the instruction + // that left the paired try + // + // practically, this makes endfinally a branch with no target + + singleTargetOffset = null; + multiTargetOffsets = ImmutableArray.Empty; + } + else + { + throw new InvalidOperationException($"Unexpected operand when processing branch {i}"); + } + + return (singleTargetOffset, multiTargetOffsets); + } - return (singleTargetOffset, multiTargetOffsets); + /// + /// Calculates which ranges of IL are unreachable, given blocks which have head and tail reachability calculated. + /// + private static ImmutableArray DetermineUnreachableRanges(ImmutableArray blocks, int lastInstructionOffset) + { + ImmutableArray.Builder ret = ImmutableArray.CreateBuilder(); + + int endOfMethodOffset = lastInstructionOffset + 1; // add 1 so we point _past_ the end of the method + + for (int curBlockIx = 0; curBlockIx < blocks.Length; curBlockIx++) + { + BasicBlock curBlock = blocks[curBlockIx]; + + int endOfCurBlockOffset; + if (curBlockIx == blocks.Length - 1) + { + endOfCurBlockOffset = endOfMethodOffset; + } + else + { + endOfCurBlockOffset = blocks[curBlockIx + 1].StartOffset - 1; // minus 1 so we don't include anything of the following block } - /// - /// Calculates which ranges of IL are unreachable, given blocks which have head and tail reachability calculated. - /// - private static ImmutableArray DetermineUnreachableRanges(ImmutableArray blocks, int lastInstructionOffset) + if (curBlock.HeadReachable) { - ImmutableArray.Builder ret = ImmutableArray.CreateBuilder(); + if (curBlock.TailReachable) + { + // it's all reachable + continue; + } - int endOfMethodOffset = lastInstructionOffset + 1; // add 1 so we point _past_ the end of the method + // tail isn't reachable, which means there's a call to something that doesn't return... + Instruction doesNotReturnInstr = curBlock.UnreachableAfter; - for (int curBlockIx = 0; curBlockIx < blocks.Length; curBlockIx++) - { - BasicBlock curBlock = blocks[curBlockIx]; - - int endOfCurBlockOffset; - if (curBlockIx == blocks.Length - 1) - { - endOfCurBlockOffset = endOfMethodOffset; - } - else - { - endOfCurBlockOffset = blocks[curBlockIx + 1].StartOffset - 1; // minus 1 so we don't include anything of the following block - } - - if (curBlock.HeadReachable) - { - if (curBlock.TailReachable) - { - // it's all reachable - continue; - } - - // tail isn't reachable, which means there's a call to something that doesn't return... - Instruction doesNotReturnInstr = curBlock.UnreachableAfter; - - // and it's everything _after_ the following instruction that is unreachable - // so record the following instruction through the end of the block - Instruction followingInstr = doesNotReturnInstr.Next; - - ret.Add(new UnreachableRange(followingInstr.Offset, endOfCurBlockOffset)); - } - else - { - // none of it is reachable - ret.Add(new UnreachableRange(curBlock.StartOffset, endOfCurBlockOffset)); - } - } + // and it's everything _after_ the following instruction that is unreachable + // so record the following instruction through the end of the block + Instruction followingInstr = doesNotReturnInstr.Next; - return ret.ToImmutable(); + ret.Add(new UnreachableRange(followingInstr.Offset, endOfCurBlockOffset)); } + else + { + // none of it is reachable + ret.Add(new UnreachableRange(curBlock.StartOffset, endOfCurBlockOffset)); + } + } + + return ret.ToImmutable(); + } + + /// + /// Process all the blocks and determine if their first instruction is reachable, + /// that is if they have "head reachability". + /// + /// "Tail reachability" will have already been determined in CreateBlocks. + /// + private static void DetermineHeadReachability(ImmutableArray blocks) + { + var blockLookup = blocks.ToImmutableDictionary(b => b.StartOffset); - /// - /// Process all the blocks and determine if their first instruction is reachable, - /// that is if they have "head reachability". - /// - /// "Tail reachability" will have already been determined in CreateBlocks. - /// - private static void DetermineHeadReachability(ImmutableArray blocks) + BasicBlock headBlock = blockLookup[0]; + + var knownLive = ImmutableStack.Create(headBlock); + + while (!knownLive.IsEmpty) + { + knownLive = knownLive.Pop(out BasicBlock block); + + if (block.HeadReachable) { - var blockLookup = blocks.ToImmutableDictionary(b => b.StartOffset); + // already seen this block + continue; + } - BasicBlock headBlock = blockLookup[0]; + // we can reach this block, clearly + block.HeadReachable = true; - var knownLive = ImmutableStack.Create(headBlock); + if (block.TailReachable) + { + // we can reach all the blocks it might flow to + foreach (int reachableOffset in block.BranchesTo) + { + BasicBlock reachableBlock = blockLookup[reachableOffset]; + knownLive = knownLive.Push(reachableBlock); + } + } - while (!knownLive.IsEmpty) - { - knownLive = knownLive.Pop(out BasicBlock block); - - if (block.HeadReachable) - { - // already seen this block - continue; - } - - // we can reach this block, clearly - block.HeadReachable = true; - - if (block.TailReachable) - { - // we can reach all the blocks it might flow to - foreach (int reachableOffset in block.BranchesTo) - { - BasicBlock reachableBlock = blockLookup[reachableOffset]; - knownLive = knownLive.Push(reachableBlock); - } - } - - // if the block is covered by an exception handler, then executing _any_ instruction in it - // could conceivably cause those handlers to be visited - foreach (int exceptionHandlerOffset in block.ExceptionBranchesTo) - { - BasicBlock reachableHandler = blockLookup[exceptionHandlerOffset]; - knownLive = knownLive.Push(reachableHandler); - } - } + // if the block is covered by an exception handler, then executing _any_ instruction in it + // could conceivably cause those handlers to be visited + foreach (int exceptionHandlerOffset in block.ExceptionBranchesTo) + { + BasicBlock reachableHandler = blockLookup[exceptionHandlerOffset]; + knownLive = knownLive.Push(reachableHandler); } + } + } - /// - /// Create BasicBlocks from an instruction stream, exception blocks, and branches. - /// - /// Each block starts either at the start of the method, immediately after a branch or at a target for a branch, - /// and ends with another branch, another branch target, or the end of the method. - /// - /// "Tail reachability" is also calculated, which is whether the block can ever actually get past its last instruction. - /// - private ImmutableArray CreateBasicBlocks(Collection instrs, Collection exceptionHandlers, ImmutableArray branches) + /// + /// Create BasicBlocks from an instruction stream, exception blocks, and branches. + /// + /// Each block starts either at the start of the method, immediately after a branch or at a target for a branch, + /// and ends with another branch, another branch target, or the end of the method. + /// + /// "Tail reachability" is also calculated, which is whether the block can ever actually get past its last instruction. + /// + private ImmutableArray CreateBasicBlocks(Collection instrs, Collection exceptionHandlers, ImmutableArray branches) + { + // every branch-like instruction starts or stops a block + ILookup branchInstrLocs = branches.ToLookup(i => i.Offset); + var branchInstrOffsets = branchInstrLocs.Select(k => k.Key).ToImmutableHashSet(); + + // every target that might be branched to starts or stops a block + ImmutableHashSet.Builder branchTargetOffsetsBuilder = ImmutableHashSet.CreateBuilder(); + foreach (BranchInstruction branch in branches) + { + if (branch.HasMultiTargets) { - // every branch-like instruction starts or stops a block - ILookup branchInstrLocs = branches.ToLookup(i => i.Offset); - var branchInstrOffsets = branchInstrLocs.Select(k => k.Key).ToImmutableHashSet(); + foreach (int target in branch.TargetOffsets) + { + branchTargetOffsetsBuilder.Add(target); + } + } + else + { + branchTargetOffsetsBuilder.Add(branch.TargetOffset); + } + } - // every target that might be branched to starts or stops a block - ImmutableHashSet.Builder branchTargetOffsetsBuilder = ImmutableHashSet.CreateBuilder(); - foreach (BranchInstruction branch in branches) - { - if (branch.HasMultiTargets) - { - foreach (int target in branch.TargetOffsets) - { - branchTargetOffsetsBuilder.Add(target); - } - } - else - { - branchTargetOffsetsBuilder.Add(branch.TargetOffset); - } - } + // every exception handler an entry point + // either it's handler, or it's filter (if present) + foreach (ExceptionHandler handler in exceptionHandlers) + { + if (handler.FilterStart != null) + { + branchTargetOffsetsBuilder.Add(handler.FilterStart.Offset); + } + else + { + branchTargetOffsetsBuilder.Add(handler.HandlerStart.Offset); + } + } - // every exception handler an entry point - // either it's handler, or it's filter (if present) - foreach (ExceptionHandler handler in exceptionHandlers) - { - if (handler.FilterStart != null) - { - branchTargetOffsetsBuilder.Add(handler.FilterStart.Offset); - } - else - { - branchTargetOffsetsBuilder.Add(handler.HandlerStart.Offset); - } - } + ImmutableHashSet branchTargetOffsets = branchTargetOffsetsBuilder.ToImmutable(); - ImmutableHashSet branchTargetOffsets = branchTargetOffsetsBuilder.ToImmutable(); + // ending the method is also important + int endOfMethodOffset = instrs[instrs.Count - 1].Offset; - // ending the method is also important - int endOfMethodOffset = instrs[instrs.Count - 1].Offset; + ImmutableArray blocks = ImmutableArray.Empty; + int? blockStartedAt = null; + Instruction unreachableAfter = null; + foreach (Instruction i in instrs) + { + int offset = i.Offset; + System.Collections.Generic.IEnumerable branchesAtLoc = branchInstrLocs[offset]; - ImmutableArray blocks = ImmutableArray.Empty; - int? blockStartedAt = null; - Instruction unreachableAfter = null; - foreach (Instruction i in instrs) - { - int offset = i.Offset; - System.Collections.Generic.IEnumerable branchesAtLoc = branchInstrLocs[offset]; - - if (blockStartedAt == null) - { - blockStartedAt = offset; - unreachableAfter = null; - } - - bool isBranch = branchInstrOffsets.Contains(offset); - bool isFollowedByBranchTarget = i.Next != null && branchTargetOffsets.Contains(i.Next.Offset); - bool isEndOfMtd = endOfMethodOffset == offset; - - if (unreachableAfter == null && DoesNotReturn(i)) - { - unreachableAfter = i; - } - - bool blockEnds = isBranch || isFollowedByBranchTarget || isEndOfMtd; - if (blockEnds) - { - Instruction nextInstr = i.Next; - - // figure out all the different places the basic block could lead to - ImmutableArray goesTo; - if (branchesAtLoc.Any()) - { - // it ends in a branch, where all does it branch? - goesTo = ImmutableArray.Empty; - foreach (BranchInstruction branch in branchesAtLoc) - { - if (branch.HasMultiTargets) - { - goesTo = goesTo.AddRange(branch.TargetOffsets); - } - else - { - goesTo = goesTo.Add(branch.TargetOffset); - } - } - } - else if (nextInstr != null) - { - // it falls throw to another instruction - goesTo = ImmutableArray.Create(nextInstr.Offset); - } - else - { - // it ends the method - goesTo = ImmutableArray.Empty; - } - - ImmutableArray exceptionSwitchesTo = ImmutableArray.Empty; - - // if the block is covered by any exception handlers then - // it is possible that it will branch to its handler block - foreach (ExceptionHandler handler in exceptionHandlers) - { - int tryStart = handler.TryStart.Offset; - int tryEnd = handler.TryEnd.Offset; - - bool containsStartOfTry = - tryStart >= blockStartedAt.Value && - tryStart <= i.Offset; - - bool containsEndOfTry = - tryEnd >= blockStartedAt.Value && - tryEnd <= i.Offset; - - bool blockInsideTry = blockStartedAt.Value >= tryStart && i.Offset <= tryEnd; - - // blocks do not necessarily align to the TRY part of exception handlers, so we need to handle three cases: - // - the try _starts_ in the block - // - the try _ends_ in the block - // - the try complete covers the block, but starts and ends before and after it (respectively) - bool tryOverlapsBlock = containsStartOfTry || containsEndOfTry || blockInsideTry; - - if (!tryOverlapsBlock) - { - continue; - } - - // if there's a filter, that runs first - if (handler.FilterStart != null) - { - exceptionSwitchesTo = exceptionSwitchesTo.Add(handler.FilterStart.Offset); - } - else - { - // otherwise, go straight to the handler - exceptionSwitchesTo = exceptionSwitchesTo.Add(handler.HandlerStart.Offset); - } - } - - blocks = blocks.Add(new BasicBlock(blockStartedAt.Value, unreachableAfter, goesTo, exceptionSwitchesTo)); - - blockStartedAt = null; - unreachableAfter = null; - } - } + if (blockStartedAt == null) + { + blockStartedAt = offset; + unreachableAfter = null; + } + + bool isBranch = branchInstrOffsets.Contains(offset); + bool isFollowedByBranchTarget = i.Next != null && branchTargetOffsets.Contains(i.Next.Offset); + bool isEndOfMtd = endOfMethodOffset == offset; - return blocks; + if (unreachableAfter == null && DoesNotReturn(i)) + { + unreachableAfter = i; } - /// - /// Returns true if the given instruction will never return, - /// and thus subsequent instructions will never be run. - /// - private bool DoesNotReturn(Instruction instr) + bool blockEnds = isBranch || isFollowedByBranchTarget || isEndOfMtd; + if (blockEnds) { - if (!IsCall(instr, out MethodReference mtd)) + Instruction nextInstr = i.Next; + + // figure out all the different places the basic block could lead to + ImmutableArray goesTo; + if (branchesAtLoc.Any()) + { + // it ends in a branch, where all does it branch? + goesTo = ImmutableArray.Empty; + foreach (BranchInstruction branch in branchesAtLoc) { - return false; + if (branch.HasMultiTargets) + { + goesTo = goesTo.AddRange(branch.TargetOffsets); + } + else + { + goesTo = goesTo.Add(branch.TargetOffset); + } + } + } + else if (nextInstr != null) + { + // it falls throw to another instruction + goesTo = ImmutableArray.Create(nextInstr.Offset); + } + else + { + // it ends the method + goesTo = ImmutableArray.Empty; + } + + ImmutableArray exceptionSwitchesTo = ImmutableArray.Empty; + + // if the block is covered by any exception handlers then + // it is possible that it will branch to its handler block + foreach (ExceptionHandler handler in exceptionHandlers) + { + int tryStart = handler.TryStart.Offset; + int tryEnd = handler.TryEnd.Offset; + + bool containsStartOfTry = + tryStart >= blockStartedAt.Value && + tryStart <= i.Offset; + + bool containsEndOfTry = + tryEnd >= blockStartedAt.Value && + tryEnd <= i.Offset; + + bool blockInsideTry = blockStartedAt.Value >= tryStart && i.Offset <= tryEnd; + + // blocks do not necessarily align to the TRY part of exception handlers, so we need to handle three cases: + // - the try _starts_ in the block + // - the try _ends_ in the block + // - the try complete covers the block, but starts and ends before and after it (respectively) + bool tryOverlapsBlock = containsStartOfTry || containsEndOfTry || blockInsideTry; + + if (!tryOverlapsBlock) + { + continue; } - return _doesNotReturnMethods.Contains(mtd.MetadataToken); - } - - /// - /// Returns true if the given instruction is a Call or Callvirt. - /// - /// If it is a call, extracts the MethodReference that is being called. - /// - private static bool IsCall(Instruction instr, out MethodReference mtd) - { - OpCode opcode = instr.OpCode; - if (opcode != OpCodes.Call && opcode != OpCodes.Callvirt) + // if there's a filter, that runs first + if (handler.FilterStart != null) { - mtd = null; - return false; + exceptionSwitchesTo = exceptionSwitchesTo.Add(handler.FilterStart.Offset); } + else + { + // otherwise, go straight to the handler + exceptionSwitchesTo = exceptionSwitchesTo.Add(handler.HandlerStart.Offset); + } + } - mtd = (MethodReference)instr.Operand; + blocks = blocks.Add(new BasicBlock(blockStartedAt.Value, unreachableAfter, goesTo, exceptionSwitchesTo)); - return true; + blockStartedAt = null; + unreachableAfter = null; } + } + + return blocks; + } + + /// + /// Returns true if the given instruction will never return, + /// and thus subsequent instructions will never be run. + /// + private bool DoesNotReturn(Instruction instr) + { + if (!IsCall(instr, out MethodReference mtd)) + { + return false; + } + + return _doesNotReturnMethods.Contains(mtd.MetadataToken); + } + + /// + /// Returns true if the given instruction is a Call or Callvirt. + /// + /// If it is a call, extracts the MethodReference that is being called. + /// + private static bool IsCall(Instruction instr, out MethodReference mtd) + { + OpCode opcode = instr.OpCode; + if (opcode != OpCodes.Call && opcode != OpCodes.Callvirt) + { + mtd = null; + return false; + } + + mtd = (MethodReference)instr.Operand; + + return true; } + } } diff --git a/src/coverlet.core/Reporters/CoberturaReporter.cs b/src/coverlet.core/Reporters/CoberturaReporter.cs index 3bbb67aaa..aa6a11742 100644 --- a/src/coverlet.core/Reporters/CoberturaReporter.cs +++ b/src/coverlet.core/Reporters/CoberturaReporter.cs @@ -13,220 +13,220 @@ namespace Coverlet.Core.Reporters { - internal class CoberturaReporter : IReporter - { - public ReporterOutputType OutputType => ReporterOutputType.File; + internal class CoberturaReporter : IReporter + { + public ReporterOutputType OutputType => ReporterOutputType.File; - public string Format => "cobertura"; + public string Format => "cobertura"; - public string Extension => "cobertura.xml"; + public string Extension => "cobertura.xml"; - public string Report(CoverageResult result, ISourceRootTranslator sourceRootTranslator) + public string Report(CoverageResult result, ISourceRootTranslator sourceRootTranslator) + { + var summary = new CoverageSummary(); + + CoverageDetails lineCoverage = summary.CalculateLineCoverage(result.Modules); + CoverageDetails branchCoverage = summary.CalculateBranchCoverage(result.Modules); + + var xml = new XDocument(); + var coverage = new XElement("coverage"); + coverage.Add(new XAttribute("line-rate", (summary.CalculateLineCoverage(result.Modules).Percent / 100).ToString(CultureInfo.InvariantCulture))); + coverage.Add(new XAttribute("branch-rate", (summary.CalculateBranchCoverage(result.Modules).Percent / 100).ToString(CultureInfo.InvariantCulture))); + coverage.Add(new XAttribute("version", "1.9")); + coverage.Add(new XAttribute("timestamp", (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds)); + + var sources = new XElement("sources"); + + var absolutePaths = new List(); + if (!result.Parameters.DeterministicReport) + { + absolutePaths = GetBasePaths(result.Modules, result.Parameters.UseSourceLink).ToList(); + absolutePaths.ForEach(x => sources.Add(new XElement("source", x))); + } + + var packages = new XElement("packages"); + foreach (KeyValuePair module in result.Modules) + { + var package = new XElement("package"); + package.Add(new XAttribute("name", Path.GetFileNameWithoutExtension(module.Key))); + package.Add(new XAttribute("line-rate", (summary.CalculateLineCoverage(module.Value).Percent / 100).ToString(CultureInfo.InvariantCulture))); + package.Add(new XAttribute("branch-rate", (summary.CalculateBranchCoverage(module.Value).Percent / 100).ToString(CultureInfo.InvariantCulture))); + package.Add(new XAttribute("complexity", summary.CalculateCyclomaticComplexity(module.Value))); + + var classes = new XElement("classes"); + foreach (KeyValuePair document in module.Value) { - var summary = new CoverageSummary(); - - CoverageDetails lineCoverage = summary.CalculateLineCoverage(result.Modules); - CoverageDetails branchCoverage = summary.CalculateBranchCoverage(result.Modules); - - var xml = new XDocument(); - var coverage = new XElement("coverage"); - coverage.Add(new XAttribute("line-rate", (summary.CalculateLineCoverage(result.Modules).Percent / 100).ToString(CultureInfo.InvariantCulture))); - coverage.Add(new XAttribute("branch-rate", (summary.CalculateBranchCoverage(result.Modules).Percent / 100).ToString(CultureInfo.InvariantCulture))); - coverage.Add(new XAttribute("version", "1.9")); - coverage.Add(new XAttribute("timestamp", (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds)); - - var sources = new XElement("sources"); - - var absolutePaths = new List(); + foreach (KeyValuePair cls in document.Value) + { + var @class = new XElement("class"); + @class.Add(new XAttribute("name", cls.Key)); + string fileName; if (!result.Parameters.DeterministicReport) { - absolutePaths = GetBasePaths(result.Modules, result.Parameters.UseSourceLink).ToList(); - absolutePaths.ForEach(x => sources.Add(new XElement("source", x))); + fileName = GetRelativePathFromBase(absolutePaths, document.Key, result.Parameters.UseSourceLink); + } + else + { + fileName = sourceRootTranslator.ResolveDeterministicPath(document.Key); } + @class.Add(new XAttribute("filename", fileName)); + @class.Add(new XAttribute("line-rate", (summary.CalculateLineCoverage(cls.Value).Percent / 100).ToString(CultureInfo.InvariantCulture))); + @class.Add(new XAttribute("branch-rate", (summary.CalculateBranchCoverage(cls.Value).Percent / 100).ToString(CultureInfo.InvariantCulture))); + @class.Add(new XAttribute("complexity", summary.CalculateCyclomaticComplexity(cls.Value))); + + var classLines = new XElement("lines"); + var methods = new XElement("methods"); - var packages = new XElement("packages"); - foreach (KeyValuePair module in result.Modules) + foreach (KeyValuePair meth in cls.Value) { - var package = new XElement("package"); - package.Add(new XAttribute("name", Path.GetFileNameWithoutExtension(module.Key))); - package.Add(new XAttribute("line-rate", (summary.CalculateLineCoverage(module.Value).Percent / 100).ToString(CultureInfo.InvariantCulture))); - package.Add(new XAttribute("branch-rate", (summary.CalculateBranchCoverage(module.Value).Percent / 100).ToString(CultureInfo.InvariantCulture))); - package.Add(new XAttribute("complexity", summary.CalculateCyclomaticComplexity(module.Value))); - - var classes = new XElement("classes"); - foreach (KeyValuePair document in module.Value) + // Skip all methods with no lines + if (meth.Value.Lines.Count == 0) + continue; + + var method = new XElement("method"); + method.Add(new XAttribute("name", meth.Key.Split(':').Last().Split('(').First())); + method.Add(new XAttribute("signature", "(" + meth.Key.Split(':').Last().Split('(').Last())); + method.Add(new XAttribute("line-rate", (summary.CalculateLineCoverage(meth.Value.Lines).Percent / 100).ToString(CultureInfo.InvariantCulture))); + method.Add(new XAttribute("branch-rate", (summary.CalculateBranchCoverage(meth.Value.Branches).Percent / 100).ToString(CultureInfo.InvariantCulture))); + method.Add(new XAttribute("complexity", summary.CalculateCyclomaticComplexity(meth.Value.Branches))); + + var lines = new XElement("lines"); + foreach (KeyValuePair ln in meth.Value.Lines) + { + bool isBranchPoint = meth.Value.Branches.Any(b => b.Line == ln.Key); + var line = new XElement("line"); + line.Add(new XAttribute("number", ln.Key.ToString())); + line.Add(new XAttribute("hits", ln.Value.ToString())); + line.Add(new XAttribute("branch", isBranchPoint.ToString())); + + if (isBranchPoint) { - foreach (KeyValuePair cls in document.Value) - { - var @class = new XElement("class"); - @class.Add(new XAttribute("name", cls.Key)); - string fileName; - if (!result.Parameters.DeterministicReport) - { - fileName = GetRelativePathFromBase(absolutePaths, document.Key, result.Parameters.UseSourceLink); - } - else - { - fileName = sourceRootTranslator.ResolveDeterministicPath(document.Key); - } - @class.Add(new XAttribute("filename", fileName)); - @class.Add(new XAttribute("line-rate", (summary.CalculateLineCoverage(cls.Value).Percent / 100).ToString(CultureInfo.InvariantCulture))); - @class.Add(new XAttribute("branch-rate", (summary.CalculateBranchCoverage(cls.Value).Percent / 100).ToString(CultureInfo.InvariantCulture))); - @class.Add(new XAttribute("complexity", summary.CalculateCyclomaticComplexity(cls.Value))); - - var classLines = new XElement("lines"); - var methods = new XElement("methods"); - - foreach (KeyValuePair meth in cls.Value) - { - // Skip all methods with no lines - if (meth.Value.Lines.Count == 0) - continue; - - var method = new XElement("method"); - method.Add(new XAttribute("name", meth.Key.Split(':').Last().Split('(').First())); - method.Add(new XAttribute("signature", "(" + meth.Key.Split(':').Last().Split('(').Last())); - method.Add(new XAttribute("line-rate", (summary.CalculateLineCoverage(meth.Value.Lines).Percent / 100).ToString(CultureInfo.InvariantCulture))); - method.Add(new XAttribute("branch-rate", (summary.CalculateBranchCoverage(meth.Value.Branches).Percent / 100).ToString(CultureInfo.InvariantCulture))); - method.Add(new XAttribute("complexity", summary.CalculateCyclomaticComplexity(meth.Value.Branches))); - - var lines = new XElement("lines"); - foreach (KeyValuePair ln in meth.Value.Lines) - { - bool isBranchPoint = meth.Value.Branches.Any(b => b.Line == ln.Key); - var line = new XElement("line"); - line.Add(new XAttribute("number", ln.Key.ToString())); - line.Add(new XAttribute("hits", ln.Value.ToString())); - line.Add(new XAttribute("branch", isBranchPoint.ToString())); - - if (isBranchPoint) - { - var branches = meth.Value.Branches.Where(b => b.Line == ln.Key).ToList(); - CoverageDetails branchInfoCoverage = summary.CalculateBranchCoverage(branches); - line.Add(new XAttribute("condition-coverage", $"{branchInfoCoverage.Percent.ToString(CultureInfo.InvariantCulture)}% ({branchInfoCoverage.Covered.ToString(CultureInfo.InvariantCulture)}/{branchInfoCoverage.Total.ToString(CultureInfo.InvariantCulture)})")); - var conditions = new XElement("conditions"); - var byOffset = branches.GroupBy(b => b.Offset).ToDictionary(b => b.Key, b => b.ToList()); - foreach (KeyValuePair> entry in byOffset) - { - var condition = new XElement("condition"); - condition.Add(new XAttribute("number", entry.Key)); - condition.Add(new XAttribute("type", entry.Value.Count > 2 ? "switch" : "jump")); // Just guessing here - condition.Add(new XAttribute("coverage", $"{summary.CalculateBranchCoverage(entry.Value).Percent.ToString(CultureInfo.InvariantCulture)}%")); - conditions.Add(condition); - } - - line.Add(conditions); - } - - lines.Add(line); - classLines.Add(line); - } - - method.Add(lines); - methods.Add(method); - } - - @class.Add(methods); - @class.Add(classLines); - classes.Add(@class); - } + var branches = meth.Value.Branches.Where(b => b.Line == ln.Key).ToList(); + CoverageDetails branchInfoCoverage = summary.CalculateBranchCoverage(branches); + line.Add(new XAttribute("condition-coverage", $"{branchInfoCoverage.Percent.ToString(CultureInfo.InvariantCulture)}% ({branchInfoCoverage.Covered.ToString(CultureInfo.InvariantCulture)}/{branchInfoCoverage.Total.ToString(CultureInfo.InvariantCulture)})")); + var conditions = new XElement("conditions"); + var byOffset = branches.GroupBy(b => b.Offset).ToDictionary(b => b.Key, b => b.ToList()); + foreach (KeyValuePair> entry in byOffset) + { + var condition = new XElement("condition"); + condition.Add(new XAttribute("number", entry.Key)); + condition.Add(new XAttribute("type", entry.Value.Count > 2 ? "switch" : "jump")); // Just guessing here + condition.Add(new XAttribute("coverage", $"{summary.CalculateBranchCoverage(entry.Value).Percent.ToString(CultureInfo.InvariantCulture)}%")); + conditions.Add(condition); + } + + line.Add(conditions); } - package.Add(classes); - packages.Add(package); + lines.Add(line); + classLines.Add(line); + } + + method.Add(lines); + methods.Add(method); } - coverage.Add(new XAttribute("lines-covered", lineCoverage.Covered.ToString(CultureInfo.InvariantCulture))); - coverage.Add(new XAttribute("lines-valid", lineCoverage.Total.ToString(CultureInfo.InvariantCulture))); - coverage.Add(new XAttribute("branches-covered", branchCoverage.Covered.ToString(CultureInfo.InvariantCulture))); - coverage.Add(new XAttribute("branches-valid", branchCoverage.Total.ToString(CultureInfo.InvariantCulture))); + @class.Add(methods); + @class.Add(classLines); + classes.Add(@class); + } + } + + package.Add(classes); + packages.Add(package); + } - coverage.Add(sources); - coverage.Add(packages); - xml.Add(coverage); + coverage.Add(new XAttribute("lines-covered", lineCoverage.Covered.ToString(CultureInfo.InvariantCulture))); + coverage.Add(new XAttribute("lines-valid", lineCoverage.Total.ToString(CultureInfo.InvariantCulture))); + coverage.Add(new XAttribute("branches-covered", branchCoverage.Covered.ToString(CultureInfo.InvariantCulture))); + coverage.Add(new XAttribute("branches-valid", branchCoverage.Total.ToString(CultureInfo.InvariantCulture))); - var stream = new MemoryStream(); - xml.Save(stream); + coverage.Add(sources); + coverage.Add(packages); + xml.Add(coverage); - return Encoding.UTF8.GetString(stream.ToArray()); - } + var stream = new MemoryStream(); + xml.Save(stream); - private static IEnumerable GetBasePaths(Modules modules, bool useSourceLink) + return Encoding.UTF8.GetString(stream.ToArray()); + } + + private static IEnumerable GetBasePaths(Modules modules, bool useSourceLink) + { + /* + Workflow + + Path1 c:\dir1\dir2\file1.cs + Path2 c:\dir1\file2.cs + Path3 e:\dir1\file2.cs + + 1) Search for root dir + c:\ -> c:\dir1\dir2\file1.cs + c:\dir1\file2.cs + e:\ -> e:\dir1\file2.cs + + 2) Split path on directory separator i.e. for record c:\ ordered ascending by fragment elements + Path1 = [c:|dir1|file2.cs] + Path2 = [c:|dir1|dir2|file1.cs] + + 3) Find longest shared path comparing indexes + Path1[0] = Path2[0], ..., PathY[0] -> add to final fragment list + Path1[n] = Path2[n], ..., PathY[n] -> add to final fragment list + Path1[n+1] != Path2[n+1], ..., PathY[n+1] -> break, Path1[n] was last shared fragment + + 4) Concat created fragment list + */ + if (useSourceLink) + { + return new[] { string.Empty }; + } + + return modules.Values.SelectMany(k => k.Keys).GroupBy(Directory.GetDirectoryRoot).Select(group => + { + var splittedPaths = group.Select(absolutePath => absolutePath.Split(Path.DirectorySeparatorChar)) + .OrderBy(absolutePath => absolutePath.Length).ToList(); + if (splittedPaths.Count == 1) { - /* - Workflow - - Path1 c:\dir1\dir2\file1.cs - Path2 c:\dir1\file2.cs - Path3 e:\dir1\file2.cs - - 1) Search for root dir - c:\ -> c:\dir1\dir2\file1.cs - c:\dir1\file2.cs - e:\ -> e:\dir1\file2.cs - - 2) Split path on directory separator i.e. for record c:\ ordered ascending by fragment elements - Path1 = [c:|dir1|file2.cs] - Path2 = [c:|dir1|dir2|file1.cs] - - 3) Find longest shared path comparing indexes - Path1[0] = Path2[0], ..., PathY[0] -> add to final fragment list - Path1[n] = Path2[n], ..., PathY[n] -> add to final fragment list - Path1[n+1] != Path2[n+1], ..., PathY[n+1] -> break, Path1[n] was last shared fragment - - 4) Concat created fragment list - */ - if (useSourceLink) - { - return new[] { string.Empty }; - } + return group.Key; + } - return modules.Values.SelectMany(k => k.Keys).GroupBy(Directory.GetDirectoryRoot).Select(group => - { - var splittedPaths = group.Select(absolutePath => absolutePath.Split(Path.DirectorySeparatorChar)) - .OrderBy(absolutePath => absolutePath.Length).ToList(); - if (splittedPaths.Count == 1) + var basePathFragments = new List(); + bool stopSearch = false; + splittedPaths[0].Select((value, index) => (value, index)).ToList().ForEach(fragmentIndexPair => + { + if (stopSearch) { - return group.Key; + return; } - var basePathFragments = new List(); - bool stopSearch = false; - splittedPaths[0].Select((value, index) => (value, index)).ToList().ForEach(fragmentIndexPair => + if (splittedPaths.All(sp => fragmentIndexPair.value.Equals(sp[fragmentIndexPair.index]))) { - if (stopSearch) - { - return; - } - - if (splittedPaths.All(sp => fragmentIndexPair.value.Equals(sp[fragmentIndexPair.index]))) - { - basePathFragments.Add(fragmentIndexPair.value); - } - else - { - stopSearch = true; - } - }); - return string.Concat(string.Join(Path.DirectorySeparatorChar.ToString(), basePathFragments), Path.DirectorySeparatorChar); - }); - } - - private static string GetRelativePathFromBase(IEnumerable basePaths, string path, bool useSourceLink) - { - if (useSourceLink) - { - return path; - } - - foreach (string basePath in basePaths) - { - if (path.StartsWith(basePath)) + basePathFragments.Add(fragmentIndexPair.value); + } + else { - return path.Substring(basePath.Length); + stopSearch = true; } - } - return path; + }); + return string.Concat(string.Join(Path.DirectorySeparatorChar.ToString(), basePathFragments), Path.DirectorySeparatorChar); + }); + } + + private static string GetRelativePathFromBase(IEnumerable basePaths, string path, bool useSourceLink) + { + if (useSourceLink) + { + return path; + } + + foreach (string basePath in basePaths) + { + if (path.StartsWith(basePath)) + { + return path.Substring(basePath.Length); } + } + return path; } + } } diff --git a/src/coverlet.core/Reporters/LcovReporter.cs b/src/coverlet.core/Reporters/LcovReporter.cs index 5b7471f42..66cbc9b64 100644 --- a/src/coverlet.core/Reporters/LcovReporter.cs +++ b/src/coverlet.core/Reporters/LcovReporter.cs @@ -8,68 +8,68 @@ namespace Coverlet.Core.Reporters { - internal class LcovReporter : IReporter - { - public ReporterOutputType OutputType => ReporterOutputType.File; + internal class LcovReporter : IReporter + { + public ReporterOutputType OutputType => ReporterOutputType.File; + + public string Format => "lcov"; + + public string Extension => "info"; - public string Format => "lcov"; + public string Report(CoverageResult result, ISourceRootTranslator sourceRootTranslator) + { + if (result.Parameters.DeterministicReport) + { + throw new NotSupportedException("Deterministic report not supported by lcov reporter"); + } - public string Extension => "info"; + var summary = new CoverageSummary(); + var lcov = new List(); - public string Report(CoverageResult result, ISourceRootTranslator sourceRootTranslator) + foreach (KeyValuePair module in result.Modules) + { + foreach (KeyValuePair doc in module.Value) { - if (result.Parameters.DeterministicReport) + CoverageDetails docLineCoverage = summary.CalculateLineCoverage(doc.Value); + CoverageDetails docBranchCoverage = summary.CalculateBranchCoverage(doc.Value); + CoverageDetails docMethodCoverage = summary.CalculateMethodCoverage(doc.Value); + + lcov.Add("SF:" + doc.Key); + foreach (KeyValuePair @class in doc.Value) + { + foreach (KeyValuePair method in @class.Value) { - throw new NotSupportedException("Deterministic report not supported by lcov reporter"); - } + // Skip all methods with no lines + if (method.Value.Lines.Count == 0) + continue; - var summary = new CoverageSummary(); - var lcov = new List(); + lcov.Add($"FN:{method.Value.Lines.First().Key - 1},{method.Key}"); + lcov.Add($"FNDA:{method.Value.Lines.First().Value},{method.Key}"); - foreach (KeyValuePair module in result.Modules) - { - foreach (KeyValuePair doc in module.Value) - { - CoverageDetails docLineCoverage = summary.CalculateLineCoverage(doc.Value); - CoverageDetails docBranchCoverage = summary.CalculateBranchCoverage(doc.Value); - CoverageDetails docMethodCoverage = summary.CalculateMethodCoverage(doc.Value); - - lcov.Add("SF:" + doc.Key); - foreach (KeyValuePair @class in doc.Value) - { - foreach (KeyValuePair method in @class.Value) - { - // Skip all methods with no lines - if (method.Value.Lines.Count == 0) - continue; - - lcov.Add($"FN:{method.Value.Lines.First().Key - 1},{method.Key}"); - lcov.Add($"FNDA:{method.Value.Lines.First().Value},{method.Key}"); - - foreach (KeyValuePair line in method.Value.Lines) - lcov.Add($"DA:{line.Key},{line.Value}"); - - foreach (BranchInfo branch in method.Value.Branches) - { - lcov.Add($"BRDA:{branch.Line},{branch.Offset},{branch.Path},{branch.Hits}"); - } - } - } - - lcov.Add($"LF:{docLineCoverage.Total}"); - lcov.Add($"LH:{docLineCoverage.Covered}"); - - lcov.Add($"BRF:{docBranchCoverage.Total}"); - lcov.Add($"BRH:{docBranchCoverage.Covered}"); - - lcov.Add($"FNF:{docMethodCoverage.Total}"); - lcov.Add($"FNH:{docMethodCoverage.Covered}"); - - lcov.Add("end_of_record"); - } + foreach (KeyValuePair line in method.Value.Lines) + lcov.Add($"DA:{line.Key},{line.Value}"); + + foreach (BranchInfo branch in method.Value.Branches) + { + lcov.Add($"BRDA:{branch.Line},{branch.Offset},{branch.Path},{branch.Hits}"); + } } + } + + lcov.Add($"LF:{docLineCoverage.Total}"); + lcov.Add($"LH:{docLineCoverage.Covered}"); + + lcov.Add($"BRF:{docBranchCoverage.Total}"); + lcov.Add($"BRH:{docBranchCoverage.Covered}"); - return string.Join(Environment.NewLine, lcov); + lcov.Add($"FNF:{docMethodCoverage.Total}"); + lcov.Add($"FNH:{docMethodCoverage.Covered}"); + + lcov.Add("end_of_record"); } + } + + return string.Join(Environment.NewLine, lcov); } + } } diff --git a/src/coverlet.core/Reporters/OpenCoverReporter.cs b/src/coverlet.core/Reporters/OpenCoverReporter.cs index 2e42c2894..21dfe94da 100644 --- a/src/coverlet.core/Reporters/OpenCoverReporter.cs +++ b/src/coverlet.core/Reporters/OpenCoverReporter.cs @@ -11,247 +11,247 @@ namespace Coverlet.Core.Reporters { - internal class OpenCoverReporter : IReporter + internal class OpenCoverReporter : IReporter + { + public ReporterOutputType OutputType => ReporterOutputType.File; + + public string Format => "opencover"; + + public string Extension => "opencover.xml"; + + public string Report(CoverageResult result, ISourceRootTranslator sourceRootTranslator) { - public ReporterOutputType OutputType => ReporterOutputType.File; + if (result.Parameters.DeterministicReport) + { + throw new NotSupportedException("Deterministic report not supported by openCover reporter"); + } - public string Format => "opencover"; + var summary = new CoverageSummary(); + var xml = new XDocument(); + var coverage = new XElement("CoverageSession"); + var coverageSummary = new XElement("Summary"); + var modules = new XElement("Modules"); - public string Extension => "opencover.xml"; + int numClasses = 0, numMethods = 0; + int visitedClasses = 0, visitedMethods = 0; - public string Report(CoverageResult result, ISourceRootTranslator sourceRootTranslator) - { - if (result.Parameters.DeterministicReport) - { - throw new NotSupportedException("Deterministic report not supported by openCover reporter"); - } + int i = 1; - var summary = new CoverageSummary(); - var xml = new XDocument(); - var coverage = new XElement("CoverageSession"); - var coverageSummary = new XElement("Summary"); - var modules = new XElement("Modules"); + foreach (System.Collections.Generic.KeyValuePair mod in result.Modules) + { + var module = new XElement("Module"); + module.Add(new XAttribute("hash", Guid.NewGuid().ToString().ToUpper())); - int numClasses = 0, numMethods = 0; - int visitedClasses = 0, visitedMethods = 0; + var path = new XElement("ModulePath", mod.Key); + var time = new XElement("ModuleTime", DateTime.UtcNow.ToString("yyyy-MM-ddThh:mm:ss")); + var name = new XElement("ModuleName", Path.GetFileNameWithoutExtension(mod.Key)); - int i = 1; + module.Add(path); + module.Add(time); + module.Add(name); - foreach (System.Collections.Generic.KeyValuePair mod in result.Modules) - { - var module = new XElement("Module"); - module.Add(new XAttribute("hash", Guid.NewGuid().ToString().ToUpper())); + var files = new XElement("Files"); + var classes = new XElement("Classes"); + + foreach (System.Collections.Generic.KeyValuePair doc in mod.Value) + { + var file = new XElement("File"); + file.Add(new XAttribute("uid", i.ToString())); + file.Add(new XAttribute("fullPath", doc.Key)); + files.Add(file); - var path = new XElement("ModulePath", mod.Key); - var time = new XElement("ModuleTime", DateTime.UtcNow.ToString("yyyy-MM-ddThh:mm:ss")); - var name = new XElement("ModuleName", Path.GetFileNameWithoutExtension(mod.Key)); + foreach (System.Collections.Generic.KeyValuePair cls in doc.Value) + { + var @class = new XElement("Class"); + var classSummary = new XElement("Summary"); - module.Add(path); - module.Add(time); - module.Add(name); + var className = new XElement("FullName", cls.Key); - var files = new XElement("Files"); - var classes = new XElement("Classes"); + var methods = new XElement("Methods"); + int j = 0; + bool classVisited = false; - foreach (System.Collections.Generic.KeyValuePair doc in mod.Value) + foreach (System.Collections.Generic.KeyValuePair meth in cls.Value) + { + // Skip all methods with no lines + if (meth.Value.Lines.Count == 0) + continue; + + CoverageDetails methLineCoverage = summary.CalculateLineCoverage(meth.Value.Lines); + CoverageDetails methBranchCoverage = summary.CalculateBranchCoverage(meth.Value.Branches); + int methCyclomaticComplexity = summary.CalculateCyclomaticComplexity(meth.Value.Branches); + int methNpathComplexity = summary.CalculateNpathComplexity(meth.Value.Branches); + + var method = new XElement("Method"); + + method.Add(new XAttribute("cyclomaticComplexity", methCyclomaticComplexity.ToString())); + method.Add(new XAttribute("nPathComplexity", methCyclomaticComplexity.ToString())); + method.Add(new XAttribute("sequenceCoverage", methLineCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); + method.Add(new XAttribute("branchCoverage", methBranchCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); + method.Add(new XAttribute("isConstructor", meth.Key.Contains("ctor").ToString())); + method.Add(new XAttribute("isGetter", meth.Key.Contains("get_").ToString())); + method.Add(new XAttribute("isSetter", meth.Key.Contains("set_").ToString())); + method.Add(new XAttribute("isStatic", (!meth.Key.Contains("get_") || !meth.Key.Contains("set_")).ToString())); + + var methodName = new XElement("Name", meth.Key); + + var fileRef = new XElement("FileRef"); + fileRef.Add(new XAttribute("uid", i.ToString())); + + var methodPoint = new XElement("MethodPoint"); + methodPoint.Add(new XAttribute("vc", methLineCoverage.Covered.ToString())); + methodPoint.Add(new XAttribute("uspid", "0")); + methodPoint.Add(new XAttribute(XName.Get("type", "xsi"), "SequencePoint")); + methodPoint.Add(new XAttribute("ordinal", j.ToString())); + methodPoint.Add(new XAttribute("offset", j.ToString())); + methodPoint.Add(new XAttribute("sc", "0")); + methodPoint.Add(new XAttribute("sl", meth.Value.Lines.First().Key.ToString())); + methodPoint.Add(new XAttribute("ec", "1")); + methodPoint.Add(new XAttribute("el", meth.Value.Lines.Last().Key.ToString())); + methodPoint.Add(new XAttribute("bec", "0")); + methodPoint.Add(new XAttribute("bev", "0")); + methodPoint.Add(new XAttribute("fileid", i.ToString())); + + // They're really just lines + var sequencePoints = new XElement("SequencePoints"); + var branchPoints = new XElement("BranchPoints"); + var methodSummary = new XElement("Summary"); + int k = 0; + int kBr = 0; + bool methodVisited = false; + + foreach (System.Collections.Generic.KeyValuePair lines in meth.Value.Lines) + { + BranchInfo[] lineBranches = meth.Value.Branches.Where(branchInfo => branchInfo.Line == lines.Key).ToArray(); + CoverageDetails branchCoverage = summary.CalculateBranchCoverage(lineBranches); + + var sequencePoint = new XElement("SequencePoint"); + sequencePoint.Add(new XAttribute("vc", lines.Value.ToString())); + sequencePoint.Add(new XAttribute("uspid", lines.Key.ToString())); + sequencePoint.Add(new XAttribute("ordinal", k.ToString())); + sequencePoint.Add(new XAttribute("sl", lines.Key.ToString())); + sequencePoint.Add(new XAttribute("sc", "1")); + sequencePoint.Add(new XAttribute("el", lines.Key.ToString())); + sequencePoint.Add(new XAttribute("ec", "2")); + sequencePoint.Add(new XAttribute("bec", branchCoverage.Total)); + sequencePoint.Add(new XAttribute("bev", branchCoverage.Covered)); + sequencePoint.Add(new XAttribute("fileid", i.ToString())); + sequencePoints.Add(sequencePoint); + + if (lines.Value > 0) { - var file = new XElement("File"); - file.Add(new XAttribute("uid", i.ToString())); - file.Add(new XAttribute("fullPath", doc.Key)); - files.Add(file); - - foreach (System.Collections.Generic.KeyValuePair cls in doc.Value) - { - var @class = new XElement("Class"); - var classSummary = new XElement("Summary"); - - var className = new XElement("FullName", cls.Key); - - var methods = new XElement("Methods"); - int j = 0; - bool classVisited = false; - - foreach (System.Collections.Generic.KeyValuePair meth in cls.Value) - { - // Skip all methods with no lines - if (meth.Value.Lines.Count == 0) - continue; - - CoverageDetails methLineCoverage = summary.CalculateLineCoverage(meth.Value.Lines); - CoverageDetails methBranchCoverage = summary.CalculateBranchCoverage(meth.Value.Branches); - int methCyclomaticComplexity = summary.CalculateCyclomaticComplexity(meth.Value.Branches); - int methNpathComplexity = summary.CalculateNpathComplexity(meth.Value.Branches); - - var method = new XElement("Method"); - - method.Add(new XAttribute("cyclomaticComplexity", methCyclomaticComplexity.ToString())); - method.Add(new XAttribute("nPathComplexity", methCyclomaticComplexity.ToString())); - method.Add(new XAttribute("sequenceCoverage", methLineCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); - method.Add(new XAttribute("branchCoverage", methBranchCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); - method.Add(new XAttribute("isConstructor", meth.Key.Contains("ctor").ToString())); - method.Add(new XAttribute("isGetter", meth.Key.Contains("get_").ToString())); - method.Add(new XAttribute("isSetter", meth.Key.Contains("set_").ToString())); - method.Add(new XAttribute("isStatic", (!meth.Key.Contains("get_") || !meth.Key.Contains("set_")).ToString())); - - var methodName = new XElement("Name", meth.Key); - - var fileRef = new XElement("FileRef"); - fileRef.Add(new XAttribute("uid", i.ToString())); - - var methodPoint = new XElement("MethodPoint"); - methodPoint.Add(new XAttribute("vc", methLineCoverage.Covered.ToString())); - methodPoint.Add(new XAttribute("uspid", "0")); - methodPoint.Add(new XAttribute(XName.Get("type", "xsi"), "SequencePoint")); - methodPoint.Add(new XAttribute("ordinal", j.ToString())); - methodPoint.Add(new XAttribute("offset", j.ToString())); - methodPoint.Add(new XAttribute("sc", "0")); - methodPoint.Add(new XAttribute("sl", meth.Value.Lines.First().Key.ToString())); - methodPoint.Add(new XAttribute("ec", "1")); - methodPoint.Add(new XAttribute("el", meth.Value.Lines.Last().Key.ToString())); - methodPoint.Add(new XAttribute("bec", "0")); - methodPoint.Add(new XAttribute("bev", "0")); - methodPoint.Add(new XAttribute("fileid", i.ToString())); - - // They're really just lines - var sequencePoints = new XElement("SequencePoints"); - var branchPoints = new XElement("BranchPoints"); - var methodSummary = new XElement("Summary"); - int k = 0; - int kBr = 0; - bool methodVisited = false; - - foreach (System.Collections.Generic.KeyValuePair lines in meth.Value.Lines) - { - BranchInfo[] lineBranches = meth.Value.Branches.Where(branchInfo => branchInfo.Line == lines.Key).ToArray(); - CoverageDetails branchCoverage = summary.CalculateBranchCoverage(lineBranches); - - var sequencePoint = new XElement("SequencePoint"); - sequencePoint.Add(new XAttribute("vc", lines.Value.ToString())); - sequencePoint.Add(new XAttribute("uspid", lines.Key.ToString())); - sequencePoint.Add(new XAttribute("ordinal", k.ToString())); - sequencePoint.Add(new XAttribute("sl", lines.Key.ToString())); - sequencePoint.Add(new XAttribute("sc", "1")); - sequencePoint.Add(new XAttribute("el", lines.Key.ToString())); - sequencePoint.Add(new XAttribute("ec", "2")); - sequencePoint.Add(new XAttribute("bec", branchCoverage.Total)); - sequencePoint.Add(new XAttribute("bev", branchCoverage.Covered)); - sequencePoint.Add(new XAttribute("fileid", i.ToString())); - sequencePoints.Add(sequencePoint); - - if (lines.Value > 0) - { - classVisited = true; - methodVisited = true; - } - - k++; - } - - foreach (BranchInfo branche in meth.Value.Branches) - { - var branchPoint = new XElement("BranchPoint"); - branchPoint.Add(new XAttribute("vc", branche.Hits.ToString())); - branchPoint.Add(new XAttribute("uspid", branche.Line.ToString())); - branchPoint.Add(new XAttribute("ordinal", branche.Ordinal.ToString())); - branchPoint.Add(new XAttribute("path", branche.Path.ToString())); - branchPoint.Add(new XAttribute("offset", branche.Offset.ToString())); - branchPoint.Add(new XAttribute("offsetend", branche.EndOffset.ToString())); - branchPoint.Add(new XAttribute("sl", branche.Line.ToString())); - branchPoint.Add(new XAttribute("fileid", i.ToString())); - branchPoints.Add(branchPoint); - kBr++; - } - - numMethods++; - if (methodVisited) - visitedMethods++; - - methodSummary.Add(new XAttribute("numSequencePoints", methLineCoverage.Total.ToString())); - methodSummary.Add(new XAttribute("visitedSequencePoints", methLineCoverage.Covered.ToString())); - methodSummary.Add(new XAttribute("numBranchPoints", methBranchCoverage.Total.ToString())); - methodSummary.Add(new XAttribute("visitedBranchPoints", methBranchCoverage.Covered.ToString())); - methodSummary.Add(new XAttribute("sequenceCoverage", methLineCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); - methodSummary.Add(new XAttribute("branchCoverage", methBranchCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); - methodSummary.Add(new XAttribute("maxCyclomaticComplexity", methCyclomaticComplexity.ToString())); - methodSummary.Add(new XAttribute("minCyclomaticComplexity", methCyclomaticComplexity.ToString())); - methodSummary.Add(new XAttribute("visitedClasses", "0")); - methodSummary.Add(new XAttribute("numClasses", "0")); - methodSummary.Add(new XAttribute("visitedMethods", methodVisited ? "1" : "0")); - methodSummary.Add(new XAttribute("numMethods", "1")); - - method.Add(methodSummary); - method.Add(new XElement("MetadataToken")); - method.Add(methodName); - method.Add(fileRef); - method.Add(sequencePoints); - method.Add(branchPoints); - method.Add(methodPoint); - methods.Add(method); - j++; - } - - numClasses++; - if (classVisited) - visitedClasses++; - - CoverageDetails classLineCoverage = summary.CalculateLineCoverage(cls.Value); - CoverageDetails classBranchCoverage = summary.CalculateBranchCoverage(cls.Value); - CoverageDetails classMethodCoverage = summary.CalculateMethodCoverage(cls.Value); - int classMaxCyclomaticComplexity = summary.CalculateMaxCyclomaticComplexity(cls.Value); - int classMinCyclomaticComplexity = summary.CalculateMinCyclomaticComplexity(cls.Value); - - classSummary.Add(new XAttribute("numSequencePoints", classLineCoverage.Total.ToString())); - classSummary.Add(new XAttribute("visitedSequencePoints", classLineCoverage.Covered.ToString())); - classSummary.Add(new XAttribute("numBranchPoints", classBranchCoverage.Total.ToString())); - classSummary.Add(new XAttribute("visitedBranchPoints", classBranchCoverage.Covered.ToString())); - classSummary.Add(new XAttribute("sequenceCoverage", classLineCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); - classSummary.Add(new XAttribute("branchCoverage", classBranchCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); - classSummary.Add(new XAttribute("maxCyclomaticComplexity", classMaxCyclomaticComplexity.ToString())); - classSummary.Add(new XAttribute("minCyclomaticComplexity", classMinCyclomaticComplexity.ToString())); - classSummary.Add(new XAttribute("visitedClasses", classVisited ? "1" : "0")); - classSummary.Add(new XAttribute("numClasses", "1")); - classSummary.Add(new XAttribute("visitedMethods", classMethodCoverage.Covered.ToString())); - classSummary.Add(new XAttribute("numMethods", classMethodCoverage.Total.ToString())); - - @class.Add(classSummary); - @class.Add(className); - @class.Add(methods); - classes.Add(@class); - } - i++; + classVisited = true; + methodVisited = true; } - module.Add(files); - module.Add(classes); - modules.Add(module); + k++; + } + + foreach (BranchInfo branche in meth.Value.Branches) + { + var branchPoint = new XElement("BranchPoint"); + branchPoint.Add(new XAttribute("vc", branche.Hits.ToString())); + branchPoint.Add(new XAttribute("uspid", branche.Line.ToString())); + branchPoint.Add(new XAttribute("ordinal", branche.Ordinal.ToString())); + branchPoint.Add(new XAttribute("path", branche.Path.ToString())); + branchPoint.Add(new XAttribute("offset", branche.Offset.ToString())); + branchPoint.Add(new XAttribute("offsetend", branche.EndOffset.ToString())); + branchPoint.Add(new XAttribute("sl", branche.Line.ToString())); + branchPoint.Add(new XAttribute("fileid", i.ToString())); + branchPoints.Add(branchPoint); + kBr++; + } + + numMethods++; + if (methodVisited) + visitedMethods++; + + methodSummary.Add(new XAttribute("numSequencePoints", methLineCoverage.Total.ToString())); + methodSummary.Add(new XAttribute("visitedSequencePoints", methLineCoverage.Covered.ToString())); + methodSummary.Add(new XAttribute("numBranchPoints", methBranchCoverage.Total.ToString())); + methodSummary.Add(new XAttribute("visitedBranchPoints", methBranchCoverage.Covered.ToString())); + methodSummary.Add(new XAttribute("sequenceCoverage", methLineCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); + methodSummary.Add(new XAttribute("branchCoverage", methBranchCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); + methodSummary.Add(new XAttribute("maxCyclomaticComplexity", methCyclomaticComplexity.ToString())); + methodSummary.Add(new XAttribute("minCyclomaticComplexity", methCyclomaticComplexity.ToString())); + methodSummary.Add(new XAttribute("visitedClasses", "0")); + methodSummary.Add(new XAttribute("numClasses", "0")); + methodSummary.Add(new XAttribute("visitedMethods", methodVisited ? "1" : "0")); + methodSummary.Add(new XAttribute("numMethods", "1")); + + method.Add(methodSummary); + method.Add(new XElement("MetadataToken")); + method.Add(methodName); + method.Add(fileRef); + method.Add(sequencePoints); + method.Add(branchPoints); + method.Add(methodPoint); + methods.Add(method); + j++; } - CoverageDetails moduleLineCoverage = summary.CalculateLineCoverage(result.Modules); - CoverageDetails moduleBranchCoverage = summary.CalculateBranchCoverage(result.Modules); - int moduleMaxCyclomaticComplexity = summary.CalculateMaxCyclomaticComplexity(result.Modules); - int moduleMinCyclomaticComplexity = summary.CalculateMinCyclomaticComplexity(result.Modules); - - coverageSummary.Add(new XAttribute("numSequencePoints", moduleLineCoverage.Total.ToString())); - coverageSummary.Add(new XAttribute("visitedSequencePoints", moduleLineCoverage.Covered.ToString())); - coverageSummary.Add(new XAttribute("numBranchPoints", moduleBranchCoverage.Total.ToString())); - coverageSummary.Add(new XAttribute("visitedBranchPoints", moduleBranchCoverage.Covered.ToString())); - coverageSummary.Add(new XAttribute("sequenceCoverage", moduleLineCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); - coverageSummary.Add(new XAttribute("branchCoverage", moduleBranchCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); - coverageSummary.Add(new XAttribute("maxCyclomaticComplexity", moduleMaxCyclomaticComplexity.ToString())); - coverageSummary.Add(new XAttribute("minCyclomaticComplexity", moduleMinCyclomaticComplexity.ToString())); - coverageSummary.Add(new XAttribute("visitedClasses", visitedClasses.ToString())); - coverageSummary.Add(new XAttribute("numClasses", numClasses.ToString())); - coverageSummary.Add(new XAttribute("visitedMethods", visitedMethods.ToString())); - coverageSummary.Add(new XAttribute("numMethods", numMethods.ToString())); - - coverage.Add(coverageSummary); - coverage.Add(modules); - xml.Add(coverage); - - var stream = new MemoryStream(); - xml.Save(stream); - - return Encoding.UTF8.GetString(stream.ToArray()); + numClasses++; + if (classVisited) + visitedClasses++; + + CoverageDetails classLineCoverage = summary.CalculateLineCoverage(cls.Value); + CoverageDetails classBranchCoverage = summary.CalculateBranchCoverage(cls.Value); + CoverageDetails classMethodCoverage = summary.CalculateMethodCoverage(cls.Value); + int classMaxCyclomaticComplexity = summary.CalculateMaxCyclomaticComplexity(cls.Value); + int classMinCyclomaticComplexity = summary.CalculateMinCyclomaticComplexity(cls.Value); + + classSummary.Add(new XAttribute("numSequencePoints", classLineCoverage.Total.ToString())); + classSummary.Add(new XAttribute("visitedSequencePoints", classLineCoverage.Covered.ToString())); + classSummary.Add(new XAttribute("numBranchPoints", classBranchCoverage.Total.ToString())); + classSummary.Add(new XAttribute("visitedBranchPoints", classBranchCoverage.Covered.ToString())); + classSummary.Add(new XAttribute("sequenceCoverage", classLineCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); + classSummary.Add(new XAttribute("branchCoverage", classBranchCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); + classSummary.Add(new XAttribute("maxCyclomaticComplexity", classMaxCyclomaticComplexity.ToString())); + classSummary.Add(new XAttribute("minCyclomaticComplexity", classMinCyclomaticComplexity.ToString())); + classSummary.Add(new XAttribute("visitedClasses", classVisited ? "1" : "0")); + classSummary.Add(new XAttribute("numClasses", "1")); + classSummary.Add(new XAttribute("visitedMethods", classMethodCoverage.Covered.ToString())); + classSummary.Add(new XAttribute("numMethods", classMethodCoverage.Total.ToString())); + + @class.Add(classSummary); + @class.Add(className); + @class.Add(methods); + classes.Add(@class); + } + i++; } + + module.Add(files); + module.Add(classes); + modules.Add(module); + } + + CoverageDetails moduleLineCoverage = summary.CalculateLineCoverage(result.Modules); + CoverageDetails moduleBranchCoverage = summary.CalculateBranchCoverage(result.Modules); + int moduleMaxCyclomaticComplexity = summary.CalculateMaxCyclomaticComplexity(result.Modules); + int moduleMinCyclomaticComplexity = summary.CalculateMinCyclomaticComplexity(result.Modules); + + coverageSummary.Add(new XAttribute("numSequencePoints", moduleLineCoverage.Total.ToString())); + coverageSummary.Add(new XAttribute("visitedSequencePoints", moduleLineCoverage.Covered.ToString())); + coverageSummary.Add(new XAttribute("numBranchPoints", moduleBranchCoverage.Total.ToString())); + coverageSummary.Add(new XAttribute("visitedBranchPoints", moduleBranchCoverage.Covered.ToString())); + coverageSummary.Add(new XAttribute("sequenceCoverage", moduleLineCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); + coverageSummary.Add(new XAttribute("branchCoverage", moduleBranchCoverage.Percent.ToString("G", CultureInfo.InvariantCulture))); + coverageSummary.Add(new XAttribute("maxCyclomaticComplexity", moduleMaxCyclomaticComplexity.ToString())); + coverageSummary.Add(new XAttribute("minCyclomaticComplexity", moduleMinCyclomaticComplexity.ToString())); + coverageSummary.Add(new XAttribute("visitedClasses", visitedClasses.ToString())); + coverageSummary.Add(new XAttribute("numClasses", numClasses.ToString())); + coverageSummary.Add(new XAttribute("visitedMethods", visitedMethods.ToString())); + coverageSummary.Add(new XAttribute("numMethods", numMethods.ToString())); + + coverage.Add(coverageSummary); + coverage.Add(modules); + xml.Add(coverage); + + var stream = new MemoryStream(); + xml.Save(stream); + + return Encoding.UTF8.GetString(stream.ToArray()); } + } } diff --git a/src/coverlet.core/Reporters/ReporterFactory.cs b/src/coverlet.core/Reporters/ReporterFactory.cs index 2c9dc95ee..cacd19c34 100644 --- a/src/coverlet.core/Reporters/ReporterFactory.cs +++ b/src/coverlet.core/Reporters/ReporterFactory.cs @@ -7,27 +7,27 @@ namespace Coverlet.Core.Reporters { - internal class ReporterFactory - { - private readonly string _format; - private readonly IReporter[] _reporters; + internal class ReporterFactory + { + private readonly string _format; + private readonly IReporter[] _reporters; - public ReporterFactory(string format) - { - _format = format; - _reporters = new IReporter[] { + public ReporterFactory(string format) + { + _format = format; + _reporters = new IReporter[] { new JsonReporter(), new LcovReporter(), new OpenCoverReporter(), new CoberturaReporter(), new TeamCityReporter() }; - } - - public bool IsValidFormat() - { - return CreateReporter() != null; - } + } - public IReporter CreateReporter() - => _reporters.FirstOrDefault(r => string.Equals(r.Format, _format, StringComparison.OrdinalIgnoreCase)); + public bool IsValidFormat() + { + return CreateReporter() != null; } + + public IReporter CreateReporter() + => _reporters.FirstOrDefault(r => string.Equals(r.Format, _format, StringComparison.OrdinalIgnoreCase)); + } } diff --git a/src/coverlet.core/Reporters/TeamCityReporter.cs b/src/coverlet.core/Reporters/TeamCityReporter.cs index 8ccb0d997..8a5f90cc0 100644 --- a/src/coverlet.core/Reporters/TeamCityReporter.cs +++ b/src/coverlet.core/Reporters/TeamCityReporter.cs @@ -8,67 +8,67 @@ namespace Coverlet.Core.Reporters { - internal class TeamCityReporter : IReporter - { - public ReporterOutputType OutputType => ReporterOutputType.Console; + internal class TeamCityReporter : IReporter + { + public ReporterOutputType OutputType => ReporterOutputType.Console; - public string Format => "teamcity"; + public string Format => "teamcity"; - public string Extension => null; + public string Extension => null; - public string Report(CoverageResult result, ISourceRootTranslator sourceRootTranslator) - { - if (result.Parameters.DeterministicReport) - { - throw new NotSupportedException("Deterministic report not supported by teamcity reporter"); - } + public string Report(CoverageResult result, ISourceRootTranslator sourceRootTranslator) + { + if (result.Parameters.DeterministicReport) + { + throw new NotSupportedException("Deterministic report not supported by teamcity reporter"); + } - // Calculate coverage - var summary = new CoverageSummary(); - CoverageDetails overallLineCoverage = summary.CalculateLineCoverage(result.Modules); - CoverageDetails overallBranchCoverage = summary.CalculateBranchCoverage(result.Modules); - CoverageDetails overallMethodCoverage = summary.CalculateMethodCoverage(result.Modules); + // Calculate coverage + var summary = new CoverageSummary(); + CoverageDetails overallLineCoverage = summary.CalculateLineCoverage(result.Modules); + CoverageDetails overallBranchCoverage = summary.CalculateBranchCoverage(result.Modules); + CoverageDetails overallMethodCoverage = summary.CalculateMethodCoverage(result.Modules); - // Report coverage - var stringBuilder = new StringBuilder(); - OutputLineCoverage(overallLineCoverage, stringBuilder); - OutputBranchCoverage(overallBranchCoverage, stringBuilder); - OutputMethodCoverage(overallMethodCoverage, stringBuilder); + // Report coverage + var stringBuilder = new StringBuilder(); + OutputLineCoverage(overallLineCoverage, stringBuilder); + OutputBranchCoverage(overallBranchCoverage, stringBuilder); + OutputMethodCoverage(overallMethodCoverage, stringBuilder); - // Return a placeholder - return stringBuilder.ToString(); - } + // Return a placeholder + return stringBuilder.ToString(); + } - private static void OutputLineCoverage(CoverageDetails coverageDetails, StringBuilder builder) - { - // The number of covered lines - OutputTeamCityServiceMessage("CodeCoverageAbsLCovered", coverageDetails.Covered, builder); + private static void OutputLineCoverage(CoverageDetails coverageDetails, StringBuilder builder) + { + // The number of covered lines + OutputTeamCityServiceMessage("CodeCoverageAbsLCovered", coverageDetails.Covered, builder); - // Line-level code coverage - OutputTeamCityServiceMessage("CodeCoverageAbsLTotal", coverageDetails.Total, builder); - } + // Line-level code coverage + OutputTeamCityServiceMessage("CodeCoverageAbsLTotal", coverageDetails.Total, builder); + } - private static void OutputBranchCoverage(CoverageDetails coverageDetails, StringBuilder builder) - { - // The number of covered branches - OutputTeamCityServiceMessage("CodeCoverageAbsBCovered", coverageDetails.Covered, builder); + private static void OutputBranchCoverage(CoverageDetails coverageDetails, StringBuilder builder) + { + // The number of covered branches + OutputTeamCityServiceMessage("CodeCoverageAbsBCovered", coverageDetails.Covered, builder); - // Branch-level code coverage - OutputTeamCityServiceMessage("CodeCoverageAbsBTotal", coverageDetails.Total, builder); - } + // Branch-level code coverage + OutputTeamCityServiceMessage("CodeCoverageAbsBTotal", coverageDetails.Total, builder); + } - private static void OutputMethodCoverage(CoverageDetails coverageDetails, StringBuilder builder) - { - // The number of covered methods - OutputTeamCityServiceMessage("CodeCoverageAbsMCovered", coverageDetails.Covered, builder); + private static void OutputMethodCoverage(CoverageDetails coverageDetails, StringBuilder builder) + { + // The number of covered methods + OutputTeamCityServiceMessage("CodeCoverageAbsMCovered", coverageDetails.Covered, builder); - // Method-level code coverage - OutputTeamCityServiceMessage("CodeCoverageAbsMTotal", coverageDetails.Total, builder); - } + // Method-level code coverage + OutputTeamCityServiceMessage("CodeCoverageAbsMTotal", coverageDetails.Total, builder); + } - private static void OutputTeamCityServiceMessage(string key, double value, StringBuilder builder) - { - builder.AppendLine($"##teamcity[buildStatisticValue key='{key}' value='{value.ToString("0.##", new CultureInfo("en-US"))}']"); - } + private static void OutputTeamCityServiceMessage(string key, double value, StringBuilder builder) + { + builder.AppendLine($"##teamcity[buildStatisticValue key='{key}' value='{value.ToString("0.##", new CultureInfo("en-US"))}']"); } + } } diff --git a/src/coverlet.core/Symbols/BranchPoint.cs b/src/coverlet.core/Symbols/BranchPoint.cs index 77a675e82..d600dc02f 100644 --- a/src/coverlet.core/Symbols/BranchPoint.cs +++ b/src/coverlet.core/Symbols/BranchPoint.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -6,46 +6,46 @@ namespace Coverlet.Core.Symbols { + /// + /// a branch point + /// + [DebuggerDisplay("StartLine = {StartLine}")] + internal class BranchPoint + { /// - /// a branch point + /// Line of the branching instruction /// - [DebuggerDisplay("StartLine = {StartLine}")] - internal class BranchPoint - { - /// - /// Line of the branching instruction - /// - public int StartLine { get; set; } - - /// - /// A path that can be taken - /// - public int Path { get; set; } - - /// - /// An order of the point within the method - /// - public UInt32 Ordinal { get; set; } - - /// - /// List of OffsetPoints between Offset and EndOffset (exclusive) - /// - public System.Collections.Generic.List OffsetPoints { get; set; } - - /// - /// The IL offset of the point - /// - public int Offset { get; set; } - - /// - /// Last Offset == EndOffset. - /// Can be same as Offset - /// - public int EndOffset { get; set; } - - /// - /// The url to the document if an entry was not mapped to an id - /// - public string Document { get; set; } - } -} \ No newline at end of file + public int StartLine { get; set; } + + /// + /// A path that can be taken + /// + public int Path { get; set; } + + /// + /// An order of the point within the method + /// + public UInt32 Ordinal { get; set; } + + /// + /// List of OffsetPoints between Offset and EndOffset (exclusive) + /// + public System.Collections.Generic.List OffsetPoints { get; set; } + + /// + /// The IL offset of the point + /// + public int Offset { get; set; } + + /// + /// Last Offset == EndOffset. + /// Can be same as Offset + /// + public int EndOffset { get; set; } + + /// + /// The url to the document if an entry was not mapped to an id + /// + public string Document { get; set; } + } +} diff --git a/src/coverlet.core/Symbols/CecilSymbolHelper.cs b/src/coverlet.core/Symbols/CecilSymbolHelper.cs index 930d19ede..a94f6c149 100644 --- a/src/coverlet.core/Symbols/CecilSymbolHelper.cs +++ b/src/coverlet.core/Symbols/CecilSymbolHelper.cs @@ -16,1471 +16,1471 @@ namespace Coverlet.Core.Symbols { - internal class CecilSymbolHelper : ICecilSymbolHelper + internal class CecilSymbolHelper : ICecilSymbolHelper + { + private const int StepOverLineCode = 0xFEEFEE; + // Create single instance, we cannot collide because we use full method name as key + private readonly ConcurrentDictionary _compilerGeneratedBranchesToExclude = new(); + private readonly ConcurrentDictionary> _sequencePointOffsetToSkip = new(); + + // In case of nested compiler generated classes, only the root one presents the CompilerGenerated attribute. + // So let's search up to the outermost declaring type to find the attribute + private static bool IsCompilerGenerated(MethodDefinition methodDefinition) { - private const int StepOverLineCode = 0xFEEFEE; - // Create single instance, we cannot collide because we use full method name as key - private readonly ConcurrentDictionary _compilerGeneratedBranchesToExclude = new(); - private readonly ConcurrentDictionary> _sequencePointOffsetToSkip = new(); - - // In case of nested compiler generated classes, only the root one presents the CompilerGenerated attribute. - // So let's search up to the outermost declaring type to find the attribute - private static bool IsCompilerGenerated(MethodDefinition methodDefinition) + TypeDefinition declaringType = methodDefinition.DeclaringType; + while (declaringType != null) + { + if (declaringType.CustomAttributes.Any(ca => ca.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName)) { - TypeDefinition declaringType = methodDefinition.DeclaringType; - while (declaringType != null) - { - if (declaringType.CustomAttributes.Any(ca => ca.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName)) - { - return true; - } - declaringType = declaringType.DeclaringType; - } - - return false; + return true; } + declaringType = declaringType.DeclaringType; + } - private static bool IsCompilerGenerated(FieldDefinition fieldDefinition) - { - return fieldDefinition.DeclaringType.CustomAttributes.Any(ca => ca.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName); - } + return false; + } - private static bool IsMoveNextInsideAsyncStateMachine(MethodDefinition methodDefinition) - { - if (methodDefinition.FullName.EndsWith("::MoveNext()") && IsCompilerGenerated(methodDefinition)) - { - foreach (InterfaceImplementation implementedInterface in methodDefinition.DeclaringType.Interfaces) - { - if (implementedInterface.InterfaceType.FullName == "System.Runtime.CompilerServices.IAsyncStateMachine") - { - return true; - } - } - } + private static bool IsCompilerGenerated(FieldDefinition fieldDefinition) + { + return fieldDefinition.DeclaringType.CustomAttributes.Any(ca => ca.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName); + } - return false; + private static bool IsMoveNextInsideAsyncStateMachine(MethodDefinition methodDefinition) + { + if (methodDefinition.FullName.EndsWith("::MoveNext()") && IsCompilerGenerated(methodDefinition)) + { + foreach (InterfaceImplementation implementedInterface in methodDefinition.DeclaringType.Interfaces) + { + if (implementedInterface.InterfaceType.FullName == "System.Runtime.CompilerServices.IAsyncStateMachine") + { + return true; + } } + } - private static bool IsMoveNextInsideAsyncIterator(MethodDefinition methodDefinition) - { - if (methodDefinition.FullName.EndsWith("::MoveNext()") && IsCompilerGenerated(methodDefinition)) - { - foreach (InterfaceImplementation implementedInterface in methodDefinition.DeclaringType.Interfaces) - { - if (implementedInterface.InterfaceType.FullName.StartsWith("System.Collections.Generic.IAsyncEnumerator`1<")) - { - return true; - } - } - } + return false; + } - return false; + private static bool IsMoveNextInsideAsyncIterator(MethodDefinition methodDefinition) + { + if (methodDefinition.FullName.EndsWith("::MoveNext()") && IsCompilerGenerated(methodDefinition)) + { + foreach (InterfaceImplementation implementedInterface in methodDefinition.DeclaringType.Interfaces) + { + if (implementedInterface.InterfaceType.FullName.StartsWith("System.Collections.Generic.IAsyncEnumerator`1<")) + { + return true; + } } + } - private static bool IsMoveNextInsideEnumerator(MethodDefinition methodDefinition) + return false; + } + + private static bool IsMoveNextInsideEnumerator(MethodDefinition methodDefinition) + { + if (!methodDefinition.FullName.EndsWith("::MoveNext()")) + { + return false; + } + + if (methodDefinition.DeclaringType.CustomAttributes.Any(ca => ca.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName)) + { + foreach (InterfaceImplementation implementedInterface in methodDefinition.DeclaringType.Interfaces) { - if (!methodDefinition.FullName.EndsWith("::MoveNext()")) - { - return false; - } + if (implementedInterface.InterfaceType.FullName == "System.Collections.IEnumerator") + { + return true; + } + } + } - if (methodDefinition.DeclaringType.CustomAttributes.Any(ca => ca.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName)) - { - foreach (InterfaceImplementation implementedInterface in methodDefinition.DeclaringType.Interfaces) - { - if (implementedInterface.InterfaceType.FullName == "System.Collections.IEnumerator") - { - return true; - } - } - } + return false; + } - return false; - } + private static bool IsMoveNextInsideAsyncStateMachineProlog(MethodDefinition methodDefinition) + { + /* + int num = <>1__state; + IL_0000: ldarg.0 + IL_0001: ldfld ...::'<>1__state' + IL_0006: stloc.0 + */ + + return (methodDefinition.Body.Instructions[0].OpCode == OpCodes.Ldarg_0 || + methodDefinition.Body.Instructions[0].OpCode == OpCodes.Ldarg) && + + methodDefinition.Body.Instructions[1].OpCode == OpCodes.Ldfld && + ((methodDefinition.Body.Instructions[1].Operand is FieldDefinition fd && fd.Name == "<>1__state") || + (methodDefinition.Body.Instructions[1].Operand is FieldReference fr && fr.Name == "<>1__state")) && + + (methodDefinition.Body.Instructions[2].OpCode == OpCodes.Stloc && + methodDefinition.Body.Instructions[2].Operand is VariableDefinition vd && vd.Index == 0) || + methodDefinition.Body.Instructions[2].OpCode == OpCodes.Stloc_0; + } - private static bool IsMoveNextInsideAsyncStateMachineProlog(MethodDefinition methodDefinition) + private static bool SkipMoveNextPrologueBranches(Instruction instruction) + { + /* + If method is a generated MoveNext we'll skip first branches (could be a switch or a series of branches) + that check state machine value to jump to correct state (for instance after a true async call) + Check if it's a Cond_Branch on state machine current value int num = <>1__state; + We are on branch OpCode so we need to go back by max 3 operation to reach ldloc.0 the load of "num" + Max 3 because we handle following patterns + + Swich + + // switch (num) + IL_0007: ldloc.0 2 + // (no C# code) + IL_0008: switch (IL_0037, IL_003c, ... 1 + ... + + Single branch + + // if (num != 0) + IL_0007: ldloc.0 2 + // (no C# code) + IL_0008: brfalse.s IL_000c 1 + IL_000a: br.s IL_000e + IL_000c: br.s IL_0049 + IL_000e: nop + ... + + More tha one branch + + // if (num != 0) + IL_0007: ldloc.0 + // (no C# code) + IL_0008: brfalse.s IL_0012 + IL_000a: br.s IL_000c + // if (num == 1) + IL_000c: ldloc.0 3 + IL_000d: ldc.i4.1 2 + IL_000e: beq.s IL_0014 1 + // (no C# code) + IL_0010: br.s IL_0019 + IL_0012: br.s IL_0060 + IL_0014: br IL_00e5 + IL_0019: nop + ... + + so we know that current branch are checking that field and we're not interested in. + */ + + Instruction current = instruction.Previous; + for (int instructionBefore = 3; instructionBefore > 0 && current.Previous != null; current = current.Previous, instructionBefore--) + { + if ( + (current.OpCode == OpCodes.Ldloc && current.Operand is VariableDefinition vo && vo.Index == 0) || + current.OpCode == OpCodes.Ldloc_0 + ) { - /* - int num = <>1__state; - IL_0000: ldarg.0 - IL_0001: ldfld ...::'<>1__state' - IL_0006: stloc.0 - */ - - return (methodDefinition.Body.Instructions[0].OpCode == OpCodes.Ldarg_0 || - methodDefinition.Body.Instructions[0].OpCode == OpCodes.Ldarg) && - - methodDefinition.Body.Instructions[1].OpCode == OpCodes.Ldfld && - ((methodDefinition.Body.Instructions[1].Operand is FieldDefinition fd && fd.Name == "<>1__state") || - (methodDefinition.Body.Instructions[1].Operand is FieldReference fr && fr.Name == "<>1__state")) && - - (methodDefinition.Body.Instructions[2].OpCode == OpCodes.Stloc && - methodDefinition.Body.Instructions[2].Operand is VariableDefinition vd && vd.Index == 0) || - methodDefinition.Body.Instructions[2].OpCode == OpCodes.Stloc_0; + return true; } + } + return false; + } + + private static bool SkipIsCompleteAwaiters(Instruction instruction) + { + // Skip get_IsCompleted to avoid unuseful branch due to async/await state machine + if ( + instruction.Previous.Operand is MethodReference operand && + operand.Name == "get_IsCompleted" && + ( + operand.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices.TaskAwaiter") || + operand.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices.ValueTaskAwaiter") || + operand.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices.ConfiguredTaskAwaitable") || + operand.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable") + ) + && + ( + operand.DeclaringType.Scope.Name == "System.Runtime" || + operand.DeclaringType.Scope.Name == "netstandard" || + operand.DeclaringType.Scope.Name == "mscorlib" || + operand.DeclaringType.Scope.Name == "System.Threading.Tasks" || + operand.DeclaringType.Scope.Name == "System.Threading.Tasks.Extensions" + ) + ) + { + return true; + } + return false; + } + + private static bool SkipLambdaCachedField(Instruction instruction) + { + /* + Lambda cached field pattern + + IL_0074: ldloca.s 1 + IL_0076: call instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult() + IL_007b: nop + IL_007c: ldarg.0 + IL_007d: ldarg.0 + IL_007e: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1 Coverlet.Core.Samples.Tests.Issue_730/'d__1'::objects + IL_0083: ldsfld class [System.Runtime]System.Func`2 Coverlet.Core.Samples.Tests.Issue_730/'<>c'::'<>9__1_0' + IL_0088: dup + IL_0089: brtrue.s IL_00a2 -> CHECK IF CACHED FIELD IS NULL OR JUMP TO DELEGATE USAGE + + (INIT STATIC FIELD) + IL_008b: pop + IL_008c: ldsfld class Coverlet.Core.Samples.Tests.Issue_730/'<>c' Coverlet.Core.Samples.Tests.Issue_730/'<>c'::'<>9' + IL_0091: ldftn instance object Coverlet.Core.Samples.Tests.Issue_730/'<>c'::'b__1_0'(object) + IL_0097: newobj instance void class [System.Runtime]System.Func`2::.ctor(object, native int) + IL_009c: dup + IL_009d: stsfld class [System.Runtime]System.Func`2 Coverlet.Core.Samples.Tests.Issue_730/'<>c'::'<>9__1_0' + + (USE DELEGATE FIELD) + IL_00a2: call class [System.Runtime]System.Collections.Generic.IEnumerable`1 [System.Linq]System.Linq.Enumerable::Select(class [System.Runtime]System.Collections.Generic.IEnumerable`1, class [System.Runtime]System.Func`2) + */ - private static bool SkipMoveNextPrologueBranches(Instruction instruction) + Instruction current = instruction.Previous; + for (int instructionBefore = 2; instructionBefore > 0 && current.Previous != null; current = current.Previous, instructionBefore--) + { + if (current.OpCode == OpCodes.Ldsfld && current.Operand is FieldDefinition fd && + // LambdaCacheField https://github.com/dotnet/roslyn/blob/e704ca635bd6de70a0250e34c4567c7a28fa9f6d/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNameKind.cs#L31 + // https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs#L145 + fd.Name.StartsWith("<>9_") && + IsCompilerGenerated(fd)) { - /* - If method is a generated MoveNext we'll skip first branches (could be a switch or a series of branches) - that check state machine value to jump to correct state (for instance after a true async call) - Check if it's a Cond_Branch on state machine current value int num = <>1__state; - We are on branch OpCode so we need to go back by max 3 operation to reach ldloc.0 the load of "num" - Max 3 because we handle following patterns - - Swich - - // switch (num) - IL_0007: ldloc.0 2 - // (no C# code) - IL_0008: switch (IL_0037, IL_003c, ... 1 - ... - - Single branch - - // if (num != 0) - IL_0007: ldloc.0 2 - // (no C# code) - IL_0008: brfalse.s IL_000c 1 - IL_000a: br.s IL_000e - IL_000c: br.s IL_0049 - IL_000e: nop - ... - - More tha one branch - - // if (num != 0) - IL_0007: ldloc.0 - // (no C# code) - IL_0008: brfalse.s IL_0012 - IL_000a: br.s IL_000c - // if (num == 1) - IL_000c: ldloc.0 3 - IL_000d: ldc.i4.1 2 - IL_000e: beq.s IL_0014 1 - // (no C# code) - IL_0010: br.s IL_0019 - IL_0012: br.s IL_0060 - IL_0014: br IL_00e5 - IL_0019: nop - ... - - so we know that current branch are checking that field and we're not interested in. - */ - - Instruction current = instruction.Previous; - for (int instructionBefore = 3; instructionBefore > 0 && current.Previous != null; current = current.Previous, instructionBefore--) - { - if ( - (current.OpCode == OpCodes.Ldloc && current.Operand is VariableDefinition vo && vo.Index == 0) || - current.OpCode == OpCodes.Ldloc_0 - ) - { - return true; - } - } - return false; + return true; } + } + return false; + } - private static bool SkipIsCompleteAwaiters(Instruction instruction) + private static bool SkipGeneratedBranchForExceptionRethrown(List instructions, Instruction instruction) + { + /* + In case of exception re-thrown inside the catch block, + the compiler generates a branch to check if the exception reference is null. + + // Debug version: + IL_00b4: isinst [System.Runtime]System.Exception + IL_00b9: stloc.s 6 + IL_00bb: ldloc.s 6 + IL_00bd: brtrue.s IL_00c6 + + // Release version: + IL_008A: isinst[System.Runtime]System.Exception + IL_008F: dup + IL_0090: brtrue.s IL_0099 + + So we can go back to previous instructions and skip this branch if recognize that type of code block. + There are differences between debug and release configuration so we just look for the highlights. + */ + + int branchIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); + + return new[] { 2, 3 }.Any(x => branchIndex >= x && + instructions[branchIndex - x].OpCode == OpCodes.Isinst && + instructions[branchIndex - x].Operand is TypeReference tr && + tr.FullName == "System.Exception") + && + // check for throw opcode after branch + instructions.Count - branchIndex >= 3 && + instructions[branchIndex + 1].OpCode == OpCodes.Ldarg && + instructions[branchIndex + 2].OpCode == OpCodes.Ldfld && + instructions[branchIndex + 3].OpCode == OpCodes.Throw; + } + + private bool SkipGeneratedBranchesForExceptionHandlers(MethodDefinition methodDefinition, Instruction instruction, List bodyInstructions) + { + /* + This method is used to parse compiler generated code inside async state machine and find branches generated for exception catch blocks. + There are differences between debug and release configuration. Also variations of the shown examples exist e.g. multiple catch blocks, + nested catch blocks... Therefore we just look for the highlights. + Typical generated code for catch block is: + + // Debug version: + catch ... + { + // (no C# code) + IL_0028: stloc.2 + // object obj2 = <>s__1 = obj; + IL_0029: ldarg.0 + // (no C# code) + IL_002a: ldloc.2 + IL_002b: stfld object ...::'<>s__1' + // <>s__2 = 1; + IL_0030: ldarg.0 + IL_0031: ldc.i4.1 + IL_0032: stfld int32 ...::'<>s__2' <- store 1 into <>s__2 + // (no C# code) + IL_0037: leave.s IL_0039 + } // end handle + + // int num2 = <>s__2; + IL_0039: ldarg.0 + IL_003a: ldfld int32 ...::'<>s__2' <- load <>s__2 value and check if 1 + IL_003f: stloc.3 + // if (num2 == 1) + IL_0040: ldloc.3 + IL_0041: ldc.i4.1 + IL_0042: beq.s IL_0049 <- BRANCH : if <>s__2 value is 1 go to exception handler code + IL_0044: br IL_00d6 + IL_0049: nop <- start exception handler code + + // Release version: + catch ... + { + IL_001D: stloc.3 + IL_001E: ldarg.0 + IL_001F: ldloc.3 + IL_0020: stfld object Coverlet.Core.Samples.Tests.CatchBlock / 'd__0'::'<>7__wrap1' + IL_0025: ldc.i4.1 + IL_0026: stloc.2 + IL_0027: leave.s IL_0029 + } // end handler + + IL_0029: ldloc.2 + IL_002A: ldc.i4.1 + IL_002B: bne.un.s IL_00AC + + + In case of multiple catch blocks as + try + { + } + catch (ExceptionType1) + { + } + catch (ExceptionType2) + { + } + + generated IL contains multiple branches: + + // Debug version: + catch ...(type1) + { + ... + } + catch ...(type2) + { + ... + } + // int num2 = <>s__2; + IL_0039: ldarg.0 + IL_003a: ldfld int32 ...::'<>s__2' <- load <>s__2 value and check if 1 + IL_003f: stloc.3 + // if (num2 == 1) + IL_0040: ldloc.3 + IL_0041: ldc.i4.1 + IL_0042: beq.s IL_0049 <- BRANCH 1 (type 1) + IL_0044: br IL_00d6 + + // if (num2 == 2) + IL_0067: ldloc.s 4 + IL_0069: ldc.i4.2 + IL_006a: beq IL_0104 <- BRANCH 2 (type 2) + + // (no C# code) + IL_006f: br IL_0191 + + // Release version: + catch ...(type1) + { + ... + } + catch ...(type2) + { + ... + } + IL_003E: ldloc.2 + IL_003F: ldc.i4.1 + IL_0040: beq.s IL_004E + + IL_0042: ldloc.2/ + IL_0043: ldc.i4.2 + IL_0044: beq IL_00CD + + IL_0049: br IL_0147 + */ + if (!_compilerGeneratedBranchesToExclude.ContainsKey(methodDefinition.FullName)) + { + var detectedBranches = new List(); + Collection handlers = methodDefinition.Body.ExceptionHandlers; + + int numberOfCatchBlocks = 1; + foreach (ExceptionHandler handler in handlers) { - // Skip get_IsCompleted to avoid unuseful branch due to async/await state machine - if ( - instruction.Previous.Operand is MethodReference operand && - operand.Name == "get_IsCompleted" && - ( - operand.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices.TaskAwaiter") || - operand.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices.ValueTaskAwaiter") || - operand.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices.ConfiguredTaskAwaitable") || - operand.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable") - ) - && - ( - operand.DeclaringType.Scope.Name == "System.Runtime" || - operand.DeclaringType.Scope.Name == "netstandard" || - operand.DeclaringType.Scope.Name == "mscorlib" || - operand.DeclaringType.Scope.Name == "System.Threading.Tasks" || - operand.DeclaringType.Scope.Name == "System.Threading.Tasks.Extensions" - ) - ) + if (handlers.Any(h => h.HandlerStart == handler.HandlerEnd)) + { + // In case of multiple consecutive catch block + numberOfCatchBlocks++; + continue; + } + + int currentIndex = bodyInstructions.BinarySearch(handler.HandlerEnd, new InstructionByOffsetComparer()); + + for (int i = 0; i < numberOfCatchBlocks; i++) + { + // We expect the first branch after the end of the handler to be no more than five instructions away. + int firstBranchIndex = currentIndex + Enumerable.Range(1, 5).FirstOrDefault(x => + currentIndex + x < bodyInstructions.Count && + bodyInstructions[currentIndex + x].OpCode == OpCodes.Beq || + bodyInstructions[currentIndex + x].OpCode == OpCodes.Bne_Un); + + if (bodyInstructions.Count - firstBranchIndex > 2 && + bodyInstructions[firstBranchIndex - 1].OpCode == OpCodes.Ldc_I4 && + bodyInstructions[firstBranchIndex - 2].OpCode == OpCodes.Ldloc) { - return true; + detectedBranches.Add(bodyInstructions[firstBranchIndex].Offset); } - return false; + + currentIndex = firstBranchIndex; + } } - private static bool SkipLambdaCachedField(Instruction instruction) + _compilerGeneratedBranchesToExclude.TryAdd(methodDefinition.FullName, detectedBranches.ToArray()); + } + + return _compilerGeneratedBranchesToExclude[methodDefinition.FullName].Contains(instruction.Offset); + } + + private static bool SkipGeneratedBranchesForAwaitForeach(List instructions, Instruction instruction) + { + // An "await foreach" causes four additional branches to be generated + // by the compiler. We want to skip the last three, but we want to + // keep the first one. + // + // (1) At each iteration of the loop, a check that there is another + // item in the sequence. This is a branch that we want to keep, + // because it's basically "should we stay in the loop or not?", + // which is germane to code coverage testing. + // (2) A check near the end for whether the IAsyncEnumerator was ever + // obtained, so it can be disposed. + // (3) A check for whether an exception was thrown in the most recent + // loop iteration. + // (4) A check for whether the exception thrown in the most recent + // loop iteration has (at least) the type System.Exception. + // + // If we're looking at any of the last three of those four branches, + // we should be skipping it. + + int currentIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); + + return CheckForAsyncEnumerator(instructions, instruction, currentIndex) || + CheckIfExceptionThrown(instructions, instruction, currentIndex) || + CheckThrownExceptionType(instructions, instruction, currentIndex); + + // The pattern for the "should we stay in the loop or not?", which we don't + // want to skip (so we have no method to try to find it), looks like this: + // + // IL_0111: ldloca.s 4 + // IL_0113: call instance !0 valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1::GetResult() + // IL_0118: brtrue IL_0047 + // + // In Debug mode, there are additional things that happen in between + // the "call" and branch, but it's the same idea either way: branch + // if GetResult() returned true. + + static bool CheckForAsyncEnumerator(List instructions, Instruction instruction, int currentIndex) + { + // We're looking for the following pattern, which checks whether a + // compiler-generated field of type IAsyncEnumerator<> is null. + // + // IL_012b: ldarg.0 + // IL_012c: ldfld class [System.Private.CoreLib]System.Collections.Generic.IAsyncEnumerator`1 AwaitForeachStateMachine/'d__0'::'<>7__wrap1' + // IL_0131: brfalse.s IL_0196 + + if (instruction.OpCode != OpCodes.Brfalse && + instruction.OpCode != OpCodes.Brfalse_S) { - /* - Lambda cached field pattern - - IL_0074: ldloca.s 1 - IL_0076: call instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult() - IL_007b: nop - IL_007c: ldarg.0 - IL_007d: ldarg.0 - IL_007e: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1 Coverlet.Core.Samples.Tests.Issue_730/'d__1'::objects - IL_0083: ldsfld class [System.Runtime]System.Func`2 Coverlet.Core.Samples.Tests.Issue_730/'<>c'::'<>9__1_0' - IL_0088: dup - IL_0089: brtrue.s IL_00a2 -> CHECK IF CACHED FIELD IS NULL OR JUMP TO DELEGATE USAGE - - (INIT STATIC FIELD) - IL_008b: pop - IL_008c: ldsfld class Coverlet.Core.Samples.Tests.Issue_730/'<>c' Coverlet.Core.Samples.Tests.Issue_730/'<>c'::'<>9' - IL_0091: ldftn instance object Coverlet.Core.Samples.Tests.Issue_730/'<>c'::'b__1_0'(object) - IL_0097: newobj instance void class [System.Runtime]System.Func`2::.ctor(object, native int) - IL_009c: dup - IL_009d: stsfld class [System.Runtime]System.Func`2 Coverlet.Core.Samples.Tests.Issue_730/'<>c'::'<>9__1_0' - - (USE DELEGATE FIELD) - IL_00a2: call class [System.Runtime]System.Collections.Generic.IEnumerable`1 [System.Linq]System.Linq.Enumerable::Select(class [System.Runtime]System.Collections.Generic.IEnumerable`1, class [System.Runtime]System.Func`2) - */ - - Instruction current = instruction.Previous; - for (int instructionBefore = 2; instructionBefore > 0 && current.Previous != null; current = current.Previous, instructionBefore--) - { - if (current.OpCode == OpCodes.Ldsfld && current.Operand is FieldDefinition fd && - // LambdaCacheField https://github.com/dotnet/roslyn/blob/e704ca635bd6de70a0250e34c4567c7a28fa9f6d/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNameKind.cs#L31 - // https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs#L145 - fd.Name.StartsWith("<>9_") && - IsCompilerGenerated(fd)) - { - return true; - } - } - return false; + return false; } - private static bool SkipGeneratedBranchForExceptionRethrown(List instructions, Instruction instruction) + if (currentIndex >= 2 && + (instructions[currentIndex - 2].OpCode == OpCodes.Ldarg || + instructions[currentIndex - 2].OpCode == OpCodes.Ldarg_0) && + instructions[currentIndex - 1].OpCode == OpCodes.Ldfld && + ( + (instructions[currentIndex - 1].Operand is FieldDefinition field && IsCompilerGenerated(field) && field.FieldType.FullName.StartsWith("System.Collections.Generic.IAsyncEnumerator")) || + (instructions[currentIndex - 1].Operand is FieldReference fieldRef && IsCompilerGenerated(fieldRef.Resolve()) && fieldRef.FieldType.FullName.StartsWith("System.Collections.Generic.IAsyncEnumerator")) + ) + ) { - /* - In case of exception re-thrown inside the catch block, - the compiler generates a branch to check if the exception reference is null. - - // Debug version: - IL_00b4: isinst [System.Runtime]System.Exception - IL_00b9: stloc.s 6 - IL_00bb: ldloc.s 6 - IL_00bd: brtrue.s IL_00c6 - - // Release version: - IL_008A: isinst[System.Runtime]System.Exception - IL_008F: dup - IL_0090: brtrue.s IL_0099 - - So we can go back to previous instructions and skip this branch if recognize that type of code block. - There are differences between debug and release configuration so we just look for the highlights. - */ - - int branchIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); - - return new[] { 2, 3 }.Any(x => branchIndex >= x && - instructions[branchIndex - x].OpCode == OpCodes.Isinst && - instructions[branchIndex - x].Operand is TypeReference tr && - tr.FullName == "System.Exception") - && - // check for throw opcode after branch - instructions.Count - branchIndex >= 3 && - instructions[branchIndex + 1].OpCode == OpCodes.Ldarg && - instructions[branchIndex + 2].OpCode == OpCodes.Ldfld && - instructions[branchIndex + 3].OpCode == OpCodes.Throw; + return true; } - private bool SkipGeneratedBranchesForExceptionHandlers(MethodDefinition methodDefinition, Instruction instruction, List bodyInstructions) + return false; + } + + static bool CheckIfExceptionThrown(List instructions, Instruction instruction, int currentIndex) + { + // Here, we want to find a pattern where we're checking whether a + // compiler-generated field of type Object is null. To narrow our + // search down and reduce the odds of false positives, we'll also + // expect a call to GetResult() to precede the loading of the field's + // value. The basic pattern looks like this: + // + // IL_018f: ldloca.s 2 + // IL_0191: call instance void [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter::GetResult() + // IL_0196: ldarg.0 + // IL_0197: ldfld object AwaitForeachStateMachine/'d__0'::'<>7__wrap2' + // IL_019c: stloc.s 6 + // IL_019e: ldloc.s 6 + // IL_01a0: brfalse.s IL_01b9 + // + // Variants are possible (e.g., a "dup" instruction instead of a + // "stloc.s" and "ldloc.s" pair), so we'll just look for the + // highlights. + + if (instruction.OpCode != OpCodes.Brfalse && + instruction.OpCode != OpCodes.Brfalse_S) { - /* - This method is used to parse compiler generated code inside async state machine and find branches generated for exception catch blocks. - There are differences between debug and release configuration. Also variations of the shown examples exist e.g. multiple catch blocks, - nested catch blocks... Therefore we just look for the highlights. - Typical generated code for catch block is: - - // Debug version: - catch ... - { - // (no C# code) - IL_0028: stloc.2 - // object obj2 = <>s__1 = obj; - IL_0029: ldarg.0 - // (no C# code) - IL_002a: ldloc.2 - IL_002b: stfld object ...::'<>s__1' - // <>s__2 = 1; - IL_0030: ldarg.0 - IL_0031: ldc.i4.1 - IL_0032: stfld int32 ...::'<>s__2' <- store 1 into <>s__2 - // (no C# code) - IL_0037: leave.s IL_0039 - } // end handle - - // int num2 = <>s__2; - IL_0039: ldarg.0 - IL_003a: ldfld int32 ...::'<>s__2' <- load <>s__2 value and check if 1 - IL_003f: stloc.3 - // if (num2 == 1) - IL_0040: ldloc.3 - IL_0041: ldc.i4.1 - IL_0042: beq.s IL_0049 <- BRANCH : if <>s__2 value is 1 go to exception handler code - IL_0044: br IL_00d6 - IL_0049: nop <- start exception handler code - - // Release version: - catch ... - { - IL_001D: stloc.3 - IL_001E: ldarg.0 - IL_001F: ldloc.3 - IL_0020: stfld object Coverlet.Core.Samples.Tests.CatchBlock / 'd__0'::'<>7__wrap1' - IL_0025: ldc.i4.1 - IL_0026: stloc.2 - IL_0027: leave.s IL_0029 - } // end handler - - IL_0029: ldloc.2 - IL_002A: ldc.i4.1 - IL_002B: bne.un.s IL_00AC - - - In case of multiple catch blocks as - try - { - } - catch (ExceptionType1) - { - } - catch (ExceptionType2) - { - } - - generated IL contains multiple branches: - - // Debug version: - catch ...(type1) - { - ... - } - catch ...(type2) - { - ... - } - // int num2 = <>s__2; - IL_0039: ldarg.0 - IL_003a: ldfld int32 ...::'<>s__2' <- load <>s__2 value and check if 1 - IL_003f: stloc.3 - // if (num2 == 1) - IL_0040: ldloc.3 - IL_0041: ldc.i4.1 - IL_0042: beq.s IL_0049 <- BRANCH 1 (type 1) - IL_0044: br IL_00d6 - - // if (num2 == 2) - IL_0067: ldloc.s 4 - IL_0069: ldc.i4.2 - IL_006a: beq IL_0104 <- BRANCH 2 (type 2) - - // (no C# code) - IL_006f: br IL_0191 - - // Release version: - catch ...(type1) - { - ... - } - catch ...(type2) - { - ... - } - IL_003E: ldloc.2 - IL_003F: ldc.i4.1 - IL_0040: beq.s IL_004E + return false; + } - IL_0042: ldloc.2/ - IL_0043: ldc.i4.2 - IL_0044: beq IL_00CD + // We expect the field to be loaded no more than thre instructions before + // the branch, so that's how far we're willing to search for it. + int minFieldIndex = Math.Max(0, currentIndex - 3); - IL_0049: br IL_0147 - */ - if (!_compilerGeneratedBranchesToExclude.ContainsKey(methodDefinition.FullName)) + for (int i = currentIndex - 1; i >= minFieldIndex; --i) + { + if (instructions[i].OpCode == OpCodes.Ldfld && + ( + (instructions[i].Operand is FieldDefinition field && IsCompilerGenerated(field) && field.FieldType.FullName == "System.Object") || + (instructions[i].Operand is FieldReference fieldRef && IsCompilerGenerated(fieldRef.Resolve()) && fieldRef.FieldType.FullName == "System.Object") + )) + { + // We expect the call to GetResult() to be no more than four + // instructions before the loading of the field's value. + int minCallIndex = Math.Max(0, i - 4); + + for (int j = i - 1; j >= minCallIndex; --j) { - var detectedBranches = new List(); - Collection handlers = methodDefinition.Body.ExceptionHandlers; - - int numberOfCatchBlocks = 1; - foreach (ExceptionHandler handler in handlers) - { - if (handlers.Any(h => h.HandlerStart == handler.HandlerEnd)) - { - // In case of multiple consecutive catch block - numberOfCatchBlocks++; - continue; - } - - int currentIndex = bodyInstructions.BinarySearch(handler.HandlerEnd, new InstructionByOffsetComparer()); - - for (int i = 0; i < numberOfCatchBlocks; i++) - { - // We expect the first branch after the end of the handler to be no more than five instructions away. - int firstBranchIndex = currentIndex + Enumerable.Range(1, 5).FirstOrDefault(x => - currentIndex + x < bodyInstructions.Count && - bodyInstructions[currentIndex + x].OpCode == OpCodes.Beq || - bodyInstructions[currentIndex + x].OpCode == OpCodes.Bne_Un); - - if (bodyInstructions.Count - firstBranchIndex > 2 && - bodyInstructions[firstBranchIndex - 1].OpCode == OpCodes.Ldc_I4 && - bodyInstructions[firstBranchIndex - 2].OpCode == OpCodes.Ldloc) - { - detectedBranches.Add(bodyInstructions[firstBranchIndex].Offset); - } - - currentIndex = firstBranchIndex; - } - } - - _compilerGeneratedBranchesToExclude.TryAdd(methodDefinition.FullName, detectedBranches.ToArray()); + if (instructions[j].OpCode == OpCodes.Call && + instructions[j].Operand is MethodReference callRef && + callRef.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices") && + callRef.DeclaringType.FullName.Contains("TaskAwait") && + callRef.Name == "GetResult") + { + return true; + } } - - return _compilerGeneratedBranchesToExclude[methodDefinition.FullName].Contains(instruction.Offset); + } } - private static bool SkipGeneratedBranchesForAwaitForeach(List instructions, Instruction instruction) + return false; + } + + static bool CheckThrownExceptionType(List instructions, Instruction instruction, int currentIndex) + { + // In this case, we're looking for a branch generated by the compiler to + // check whether a previously-thrown exception has (at least) the type + // System.Exception, the pattern for which looks like this: + // + // IL_01db: ldloc.s 7 + // IL_01dd: isinst [System.Private.CoreLib]System.Exception + // IL_01e2: stloc.s 9 + // IL_01e4: ldloc.s 9 + // IL_01e6: brtrue.s IL_01eb + // + // Once again, variants are possible here, such as a "dup" instruction in + // place of the "stloc.s" and "ldloc.s" pair, and we'll reduce the odds of + // a false positive by requiring a "ldloc.s" instruction to precede the + // "isinst" instruction. + + if (instruction.OpCode != OpCodes.Brtrue && + instruction.OpCode != OpCodes.Brtrue_S) { - // An "await foreach" causes four additional branches to be generated - // by the compiler. We want to skip the last three, but we want to - // keep the first one. - // - // (1) At each iteration of the loop, a check that there is another - // item in the sequence. This is a branch that we want to keep, - // because it's basically "should we stay in the loop or not?", - // which is germane to code coverage testing. - // (2) A check near the end for whether the IAsyncEnumerator was ever - // obtained, so it can be disposed. - // (3) A check for whether an exception was thrown in the most recent - // loop iteration. - // (4) A check for whether the exception thrown in the most recent - // loop iteration has (at least) the type System.Exception. - // - // If we're looking at any of the last three of those four branches, - // we should be skipping it. - - int currentIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); - - return CheckForAsyncEnumerator(instructions, instruction, currentIndex) || - CheckIfExceptionThrown(instructions, instruction, currentIndex) || - CheckThrownExceptionType(instructions, instruction, currentIndex); - - // The pattern for the "should we stay in the loop or not?", which we don't - // want to skip (so we have no method to try to find it), looks like this: - // - // IL_0111: ldloca.s 4 - // IL_0113: call instance !0 valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1::GetResult() - // IL_0118: brtrue IL_0047 - // - // In Debug mode, there are additional things that happen in between - // the "call" and branch, but it's the same idea either way: branch - // if GetResult() returned true. - - static bool CheckForAsyncEnumerator(List instructions, Instruction instruction, int currentIndex) - { - // We're looking for the following pattern, which checks whether a - // compiler-generated field of type IAsyncEnumerator<> is null. - // - // IL_012b: ldarg.0 - // IL_012c: ldfld class [System.Private.CoreLib]System.Collections.Generic.IAsyncEnumerator`1 AwaitForeachStateMachine/'d__0'::'<>7__wrap1' - // IL_0131: brfalse.s IL_0196 - - if (instruction.OpCode != OpCodes.Brfalse && - instruction.OpCode != OpCodes.Brfalse_S) - { - return false; - } + return false; + } - if (currentIndex >= 2 && - (instructions[currentIndex - 2].OpCode == OpCodes.Ldarg || - instructions[currentIndex - 2].OpCode == OpCodes.Ldarg_0) && - instructions[currentIndex - 1].OpCode == OpCodes.Ldfld && - ( - (instructions[currentIndex - 1].Operand is FieldDefinition field && IsCompilerGenerated(field) && field.FieldType.FullName.StartsWith("System.Collections.Generic.IAsyncEnumerator")) || - (instructions[currentIndex - 1].Operand is FieldReference fieldRef && IsCompilerGenerated(fieldRef.Resolve()) && fieldRef.FieldType.FullName.StartsWith("System.Collections.Generic.IAsyncEnumerator")) - ) - ) - { - return true; - } + int minTypeCheckIndex = Math.Max(1, currentIndex - 3); - return false; - } + for (int i = currentIndex - 1; i >= minTypeCheckIndex; --i) + { + if (instructions[i].OpCode == OpCodes.Isinst && + instructions[i].Operand is TypeReference typeRef && + typeRef.FullName == "System.Exception" && + (instructions[i - 1].OpCode == OpCodes.Ldloc || + instructions[i - 1].OpCode == OpCodes.Ldloc_S || + instructions[i - 1].OpCode == OpCodes.Ldloc_0 || + instructions[i - 1].OpCode == OpCodes.Ldloc_1 || + instructions[i - 1].OpCode == OpCodes.Ldloc_2 || + instructions[i - 1].OpCode == OpCodes.Ldloc_3)) + { + return true; + } + } - static bool CheckIfExceptionThrown(List instructions, Instruction instruction, int currentIndex) - { - // Here, we want to find a pattern where we're checking whether a - // compiler-generated field of type Object is null. To narrow our - // search down and reduce the odds of false positives, we'll also - // expect a call to GetResult() to precede the loading of the field's - // value. The basic pattern looks like this: - // - // IL_018f: ldloca.s 2 - // IL_0191: call instance void [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter::GetResult() - // IL_0196: ldarg.0 - // IL_0197: ldfld object AwaitForeachStateMachine/'d__0'::'<>7__wrap2' - // IL_019c: stloc.s 6 - // IL_019e: ldloc.s 6 - // IL_01a0: brfalse.s IL_01b9 - // - // Variants are possible (e.g., a "dup" instruction instead of a - // "stloc.s" and "ldloc.s" pair), so we'll just look for the - // highlights. - - if (instruction.OpCode != OpCodes.Brfalse && - instruction.OpCode != OpCodes.Brfalse_S) - { - return false; - } + return false; + } + } - // We expect the field to be loaded no more than thre instructions before - // the branch, so that's how far we're willing to search for it. - int minFieldIndex = Math.Max(0, currentIndex - 3); + private static bool SkipGeneratedBranchesForAwaitUsing(List instructions, Instruction instruction) + { + int currentIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); + + return CheckForSkipDisposal(instructions, instruction, currentIndex) || + CheckForCleanup(instructions, instruction, currentIndex); + + static bool CheckForSkipDisposal(List instructions, Instruction instruction, int currentIndex) + { + // The async state machine generated for an "await using" contains a branch + // that checks whether the call to DisposeAsync() needs to be skipped. + // As it turns out, the pattern is a little different in Debug and Release + // configurations, but the idea is the same in either case: + // + // * Check whether the IAsyncDisposable that's being managed by the "await + // using" is null. If so, skip the call to DisposeAsync(). If not, + // make the call. + // + // To avoid other places where this pattern is used, we'll require it to be + // preceded by the tail end of a try/catch generated by the compiler. + // + // The primary difference between the Debug and Release versions of this + // pattern are where that IAsyncDisposable is stored. In Debug mode, it's + // in a field; in Release mode, it's in a local variable. So what we'll + // look for, generally, is a brfalse instruction that checks a value, + // followed shortly by reloading that same value and calling DisposeAsync() + // on it, preceded shortly by a store into a compiler-generated field and + // a "leave" instruction. + // + // Debug version: + // IL_0041: stfld object Coverlet.Core.Samples.Tests.AwaitUsing/'d__0'::'<>s__2' + // IL_0046: leave.s IL_0048 + // IL_0048: ldarg.0 + // IL_0049: ldfld class [System.Private.CoreLib]System.IO.MemoryStream Coverlet.Core.Samples.Tests.AwaitUsing/'d__0'::'5__1' + // IL_004e: brfalse.s IL_00b9 + // IL_0050: ldarg.0 + // IL_0051: ldfld class [System.Private.CoreLib]System.IO.MemoryStream Coverlet.Core.Samples.Tests.AwaitUsing/'d__0'::'5__1' + // IL_0056: callvirt instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask [System.Private.CoreLib]System.IAsyncDisposable::DisposeAsync() + // + // Release version: + // IL_0032: stfld object Coverlet.Core.Samples.Tests.AwaitUsing/'d__0'::'<>7__wrap1' + // IL_0037: leave.s IL_0039 + // IL_0039: ldloc.1 + // IL_003a: brfalse.s IL_0098 + // IL_003c: ldloc.1 + // IL_003d: callvirt instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask [System.Private.CoreLib]System.IAsyncDisposable::DisposeAsync() + + if (instruction.OpCode != OpCodes.Brfalse && + instruction.OpCode != OpCodes.Brfalse_S) + { + return false; + } - for (int i = currentIndex - 1; i >= minFieldIndex; --i) - { - if (instructions[i].OpCode == OpCodes.Ldfld && - ( - (instructions[i].Operand is FieldDefinition field && IsCompilerGenerated(field) && field.FieldType.FullName == "System.Object") || - (instructions[i].Operand is FieldReference fieldRef && IsCompilerGenerated(fieldRef.Resolve()) && fieldRef.FieldType.FullName == "System.Object") - )) - { - // We expect the call to GetResult() to be no more than four - // instructions before the loading of the field's value. - int minCallIndex = Math.Max(0, i - 4); - - for (int j = i - 1; j >= minCallIndex; --j) - { - if (instructions[j].OpCode == OpCodes.Call && - instructions[j].Operand is MethodReference callRef && - callRef.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices") && - callRef.DeclaringType.FullName.Contains("TaskAwait") && - callRef.Name == "GetResult") - { - return true; - } - } - } - } + bool isFollowedByDisposeAsync = false; - return false; + if (instructions[currentIndex - 1].OpCode == OpCodes.Ldfld && + instructions[currentIndex - 1].Operand is FieldDefinition field && + IsCompilerGenerated(field)) + { + int maxReloadFieldIndex = Math.Min(currentIndex + 2, instructions.Count - 2); + + for (int i = currentIndex + 1; i <= maxReloadFieldIndex; ++i) + { + if (instructions[i].OpCode == OpCodes.Ldfld && + instructions[i].Operand is FieldDefinition reloadedField && + field.Equals(reloadedField) && + instructions[i + 1].OpCode == OpCodes.Callvirt && + instructions[i + 1].Operand is MethodReference method && + method.DeclaringType.FullName == "System.IAsyncDisposable" && + method.Name == "DisposeAsync") + { + isFollowedByDisposeAsync = true; + break; } + } + } + else if ((instructions[currentIndex - 1].OpCode == OpCodes.Ldloc || + instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_S || + instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_0 || + instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_1 || + instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_2 || + instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_3) && + (instructions[currentIndex + 1].OpCode == OpCodes.Ldloc || + instructions[currentIndex + 1].OpCode == OpCodes.Ldloc_S || + instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_0 || + instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_1 || + instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_2 || + instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_3) && + instructions[currentIndex + 2].OpCode == OpCodes.Callvirt && + instructions[currentIndex + 2].Operand is MethodReference method && + method.DeclaringType.FullName == "System.IAsyncDisposable" && + method.Name == "DisposeAsync") + { + isFollowedByDisposeAsync = true; + } - static bool CheckThrownExceptionType(List instructions, Instruction instruction, int currentIndex) + if (isFollowedByDisposeAsync) + { + int minLeaveIndex = Math.Max(1, currentIndex - 4); + + for (int i = currentIndex - 1; i >= minLeaveIndex; --i) + { + if ((instructions[i].OpCode == OpCodes.Leave || + instructions[i].OpCode == OpCodes.Leave_S) && + instructions[i - 1].OpCode == OpCodes.Stfld && + instructions[i - 1].Operand is FieldDefinition storeField && + IsCompilerGenerated(storeField)) { - // In this case, we're looking for a branch generated by the compiler to - // check whether a previously-thrown exception has (at least) the type - // System.Exception, the pattern for which looks like this: - // - // IL_01db: ldloc.s 7 - // IL_01dd: isinst [System.Private.CoreLib]System.Exception - // IL_01e2: stloc.s 9 - // IL_01e4: ldloc.s 9 - // IL_01e6: brtrue.s IL_01eb - // - // Once again, variants are possible here, such as a "dup" instruction in - // place of the "stloc.s" and "ldloc.s" pair, and we'll reduce the odds of - // a false positive by requiring a "ldloc.s" instruction to precede the - // "isinst" instruction. - - if (instruction.OpCode != OpCodes.Brtrue && - instruction.OpCode != OpCodes.Brtrue_S) - { - return false; - } - - int minTypeCheckIndex = Math.Max(1, currentIndex - 3); - - for (int i = currentIndex - 1; i >= minTypeCheckIndex; --i) - { - if (instructions[i].OpCode == OpCodes.Isinst && - instructions[i].Operand is TypeReference typeRef && - typeRef.FullName == "System.Exception" && - (instructions[i - 1].OpCode == OpCodes.Ldloc || - instructions[i - 1].OpCode == OpCodes.Ldloc_S || - instructions[i - 1].OpCode == OpCodes.Ldloc_0 || - instructions[i - 1].OpCode == OpCodes.Ldloc_1 || - instructions[i - 1].OpCode == OpCodes.Ldloc_2 || - instructions[i - 1].OpCode == OpCodes.Ldloc_3)) - { - return true; - } - } - - return false; + return true; } + } } - private static bool SkipGeneratedBranchesForAwaitUsing(List instructions, Instruction instruction) + return false; + } + + static bool CheckForCleanup(List instructions, Instruction instruction, int currentIndex) + { + // The pattern we're looking for here is this: + // + // IL_00c6: ldloc.s 5 + // IL_00c8: call class [System.Private.CoreLib]System.Runtime.ExceptionServices.ExceptionDispatchInfo [System.Private.CoreLib]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Capture(class [System.Private.CoreLib]System.Exception) + // IL_00cd: callvirt instance void [System.Private.CoreLib]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Throw() + // IL_00d2: nop + // IL_00d3: ldarg.0 + // IL_00d4: ldfld int32 Coverlet.Core.Samples.Tests.AwaitUsing/'d__2'::'<>s__3' + // IL_00d9: stloc.s 6 + // IL_00db: ldloc.s 6 + // IL_00dd: ldc.i4.1 + // IL_00de: beq.s IL_00e2 + // IL_00e0: br.s IL_00e4 + // IL_00e2: leave.s IL_0115 + // + // It appears that this pattern is not generated in every "await using", + // but only in an "await using" without curly braces (i.e., that is + // scoped to the end of the method). It's also a slightly different + // pattern in Release vs. Debug (bne.un.s instead of beq.s followed by + // br.s). To be as safe as we can, we'll expect an ldc.i4 to precede + // the branch, then we want a load from a compiler-generated field within + // a few instructions before that, then we want an exception to be + // rethrown before that. + + if (instruction.OpCode != OpCodes.Beq && + instruction.OpCode != OpCodes.Beq_S && + instruction.OpCode != OpCodes.Bne_Un && + instruction.OpCode != OpCodes.Bne_Un_S) { - int currentIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); + return false; + } - return CheckForSkipDisposal(instructions, instruction, currentIndex) || - CheckForCleanup(instructions, instruction, currentIndex); + if (currentIndex >= 1 && + (instructions[currentIndex - 1].OpCode == OpCodes.Ldc_I4 || + instructions[currentIndex - 1].OpCode == OpCodes.Ldc_I4_1)) + { + int minLoadFieldIndex = Math.Max(1, currentIndex - 5); - static bool CheckForSkipDisposal(List instructions, Instruction instruction, int currentIndex) + for (int i = currentIndex - 2; i >= minLoadFieldIndex; --i) + { + if (instructions[i].OpCode == OpCodes.Ldfld && + instructions[i].Operand is FieldDefinition loadedField && + IsCompilerGenerated(loadedField)) { - // The async state machine generated for an "await using" contains a branch - // that checks whether the call to DisposeAsync() needs to be skipped. - // As it turns out, the pattern is a little different in Debug and Release - // configurations, but the idea is the same in either case: - // - // * Check whether the IAsyncDisposable that's being managed by the "await - // using" is null. If so, skip the call to DisposeAsync(). If not, - // make the call. - // - // To avoid other places where this pattern is used, we'll require it to be - // preceded by the tail end of a try/catch generated by the compiler. - // - // The primary difference between the Debug and Release versions of this - // pattern are where that IAsyncDisposable is stored. In Debug mode, it's - // in a field; in Release mode, it's in a local variable. So what we'll - // look for, generally, is a brfalse instruction that checks a value, - // followed shortly by reloading that same value and calling DisposeAsync() - // on it, preceded shortly by a store into a compiler-generated field and - // a "leave" instruction. - // - // Debug version: - // IL_0041: stfld object Coverlet.Core.Samples.Tests.AwaitUsing/'d__0'::'<>s__2' - // IL_0046: leave.s IL_0048 - // IL_0048: ldarg.0 - // IL_0049: ldfld class [System.Private.CoreLib]System.IO.MemoryStream Coverlet.Core.Samples.Tests.AwaitUsing/'d__0'::'5__1' - // IL_004e: brfalse.s IL_00b9 - // IL_0050: ldarg.0 - // IL_0051: ldfld class [System.Private.CoreLib]System.IO.MemoryStream Coverlet.Core.Samples.Tests.AwaitUsing/'d__0'::'5__1' - // IL_0056: callvirt instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask [System.Private.CoreLib]System.IAsyncDisposable::DisposeAsync() - // - // Release version: - // IL_0032: stfld object Coverlet.Core.Samples.Tests.AwaitUsing/'d__0'::'<>7__wrap1' - // IL_0037: leave.s IL_0039 - // IL_0039: ldloc.1 - // IL_003a: brfalse.s IL_0098 - // IL_003c: ldloc.1 - // IL_003d: callvirt instance valuetype [System.Private.CoreLib]System.Threading.Tasks.ValueTask [System.Private.CoreLib]System.IAsyncDisposable::DisposeAsync() - - if (instruction.OpCode != OpCodes.Brfalse && - instruction.OpCode != OpCodes.Brfalse_S) - { - return false; - } - - bool isFollowedByDisposeAsync = false; - - if (instructions[currentIndex - 1].OpCode == OpCodes.Ldfld && - instructions[currentIndex - 1].Operand is FieldDefinition field && - IsCompilerGenerated(field)) - { - int maxReloadFieldIndex = Math.Min(currentIndex + 2, instructions.Count - 2); - - for (int i = currentIndex + 1; i <= maxReloadFieldIndex; ++i) - { - if (instructions[i].OpCode == OpCodes.Ldfld && - instructions[i].Operand is FieldDefinition reloadedField && - field.Equals(reloadedField) && - instructions[i + 1].OpCode == OpCodes.Callvirt && - instructions[i + 1].Operand is MethodReference method && - method.DeclaringType.FullName == "System.IAsyncDisposable" && - method.Name == "DisposeAsync") - { - isFollowedByDisposeAsync = true; - break; - } - } - } - else if ((instructions[currentIndex - 1].OpCode == OpCodes.Ldloc || - instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_S || - instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_0 || - instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_1 || - instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_2 || - instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_3) && - (instructions[currentIndex + 1].OpCode == OpCodes.Ldloc || - instructions[currentIndex + 1].OpCode == OpCodes.Ldloc_S || - instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_0 || - instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_1 || - instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_2 || - instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_3) && - instructions[currentIndex + 2].OpCode == OpCodes.Callvirt && - instructions[currentIndex + 2].Operand is MethodReference method && - method.DeclaringType.FullName == "System.IAsyncDisposable" && - method.Name == "DisposeAsync") + int minRethrowIndex = Math.Max(0, i - 4); + + for (int j = i - 1; j >= minRethrowIndex; --j) + { + if (instructions[j].OpCode == OpCodes.Callvirt && + instructions[j].Operand is MethodReference method && + method.DeclaringType.FullName == "System.Runtime.ExceptionServices.ExceptionDispatchInfo" && + method.Name == "Throw") { - isFollowedByDisposeAsync = true; + return true; } - - if (isFollowedByDisposeAsync) - { - int minLeaveIndex = Math.Max(1, currentIndex - 4); - - for (int i = currentIndex - 1; i >= minLeaveIndex; --i) - { - if ((instructions[i].OpCode == OpCodes.Leave || - instructions[i].OpCode == OpCodes.Leave_S) && - instructions[i - 1].OpCode == OpCodes.Stfld && - instructions[i - 1].Operand is FieldDefinition storeField && - IsCompilerGenerated(storeField)) - { - return true; - } - } - } - - return false; + } } + } + } - static bool CheckForCleanup(List instructions, Instruction instruction, int currentIndex) - { - // The pattern we're looking for here is this: - // - // IL_00c6: ldloc.s 5 - // IL_00c8: call class [System.Private.CoreLib]System.Runtime.ExceptionServices.ExceptionDispatchInfo [System.Private.CoreLib]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Capture(class [System.Private.CoreLib]System.Exception) - // IL_00cd: callvirt instance void [System.Private.CoreLib]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Throw() - // IL_00d2: nop - // IL_00d3: ldarg.0 - // IL_00d4: ldfld int32 Coverlet.Core.Samples.Tests.AwaitUsing/'d__2'::'<>s__3' - // IL_00d9: stloc.s 6 - // IL_00db: ldloc.s 6 - // IL_00dd: ldc.i4.1 - // IL_00de: beq.s IL_00e2 - // IL_00e0: br.s IL_00e4 - // IL_00e2: leave.s IL_0115 - // - // It appears that this pattern is not generated in every "await using", - // but only in an "await using" without curly braces (i.e., that is - // scoped to the end of the method). It's also a slightly different - // pattern in Release vs. Debug (bne.un.s instead of beq.s followed by - // br.s). To be as safe as we can, we'll expect an ldc.i4 to precede - // the branch, then we want a load from a compiler-generated field within - // a few instructions before that, then we want an exception to be - // rethrown before that. - - if (instruction.OpCode != OpCodes.Beq && - instruction.OpCode != OpCodes.Beq_S && - instruction.OpCode != OpCodes.Bne_Un && - instruction.OpCode != OpCodes.Bne_Un_S) - { - return false; - } - - if (currentIndex >= 1 && - (instructions[currentIndex - 1].OpCode == OpCodes.Ldc_I4 || - instructions[currentIndex - 1].OpCode == OpCodes.Ldc_I4_1)) - { - int minLoadFieldIndex = Math.Max(1, currentIndex - 5); - - for (int i = currentIndex - 2; i >= minLoadFieldIndex; --i) - { - if (instructions[i].OpCode == OpCodes.Ldfld && - instructions[i].Operand is FieldDefinition loadedField && - IsCompilerGenerated(loadedField)) - { - int minRethrowIndex = Math.Max(0, i - 4); - - for (int j = i - 1; j >= minRethrowIndex; --j) - { - if (instructions[j].OpCode == OpCodes.Callvirt && - instructions[j].Operand is MethodReference method && - method.DeclaringType.FullName == "System.Runtime.ExceptionServices.ExceptionDispatchInfo" && - method.Name == "Throw") - { - return true; - } - } - } - } - } + return false; + } + } - return false; - } + // https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8 + private static bool SkipGeneratedBranchesForAsyncIterator(List instructions, Instruction instruction) + { + // There are two branch patterns that we want to eliminate in the + // MoveNext() method in compiler-generated async iterators. + // + // (1) A "switch" instruction near the beginning of MoveNext() that checks + // the state machine's current state and jumps to the right place. + // (2) A check that the compiler-generated field "<>w__disposeMode" is false, + // which is used to know whether the enumerator has been disposed (so it's + // necessary not to iterate any further). This is done in more than once + // place, but we always want to skip it. + + int currentIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); + + return CheckForStateSwitch(instructions, instruction, currentIndex) || + DisposeCheck(instructions, instruction, currentIndex); + + static bool CheckForStateSwitch(List instructions, Instruction instruction, int currentIndex) + { + // The pattern we're looking for here is this one: + // + // IL_0000: ldarg.0 + // IL_0001: ldfld int32 Test.AsyncEnumerableStateMachine/'d__0'::'<>1__state' + // IL_0006: stloc.0 + // .try + // { + // IL_0007: ldloc.0 + // IL_0008: ldc.i4.s -4 + // IL_000a: sub + // IL_000b: switch (IL_0026, IL_002b, IL_002f, IL_002f, IL_002d) + // + // The "switch" instruction is the branch we want to skip. To eliminate + // false positives, we'll search back for the "ldfld" of the compiler- + // generated "<>1__state" field, making sure it precedes it within five + // instructions. To be safe, we'll also require a "ldarg.0" instruction + // before the "ldfld". + + if (instruction.OpCode != OpCodes.Switch) + { + return false; } - // https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8 - private static bool SkipGeneratedBranchesForAsyncIterator(List instructions, Instruction instruction) - { - // There are two branch patterns that we want to eliminate in the - // MoveNext() method in compiler-generated async iterators. - // - // (1) A "switch" instruction near the beginning of MoveNext() that checks - // the state machine's current state and jumps to the right place. - // (2) A check that the compiler-generated field "<>w__disposeMode" is false, - // which is used to know whether the enumerator has been disposed (so it's - // necessary not to iterate any further). This is done in more than once - // place, but we always want to skip it. - - int currentIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); - - return CheckForStateSwitch(instructions, instruction, currentIndex) || - DisposeCheck(instructions, instruction, currentIndex); - - static bool CheckForStateSwitch(List instructions, Instruction instruction, int currentIndex) - { - // The pattern we're looking for here is this one: - // - // IL_0000: ldarg.0 - // IL_0001: ldfld int32 Test.AsyncEnumerableStateMachine/'d__0'::'<>1__state' - // IL_0006: stloc.0 - // .try - // { - // IL_0007: ldloc.0 - // IL_0008: ldc.i4.s -4 - // IL_000a: sub - // IL_000b: switch (IL_0026, IL_002b, IL_002f, IL_002f, IL_002d) - // - // The "switch" instruction is the branch we want to skip. To eliminate - // false positives, we'll search back for the "ldfld" of the compiler- - // generated "<>1__state" field, making sure it precedes it within five - // instructions. To be safe, we'll also require a "ldarg.0" instruction - // before the "ldfld". - - if (instruction.OpCode != OpCodes.Switch) - { - return false; - } + int minLoadStateFieldIndex = Math.Max(1, currentIndex - 5); - int minLoadStateFieldIndex = Math.Max(1, currentIndex - 5); + for (int i = currentIndex - 1; i >= minLoadStateFieldIndex; --i) + { + if (instructions[i].OpCode == OpCodes.Ldfld && + instructions[i].Operand is FieldDefinition field && + IsCompilerGenerated(field) && field.FullName.EndsWith("__state") && + (instructions[i - 1].OpCode == OpCodes.Ldarg || + instructions[i - 1].OpCode == OpCodes.Ldarg_0)) + { + return true; + } + } - for (int i = currentIndex - 1; i >= minLoadStateFieldIndex; --i) - { - if (instructions[i].OpCode == OpCodes.Ldfld && - instructions[i].Operand is FieldDefinition field && - IsCompilerGenerated(field) && field.FullName.EndsWith("__state") && - (instructions[i - 1].OpCode == OpCodes.Ldarg || - instructions[i - 1].OpCode == OpCodes.Ldarg_0)) - { - return true; - } - } + return false; + } + + static bool DisposeCheck(List instructions, Instruction instruction, int currentIndex) + { + // Within the compiler-generated async iterator, there are at least a + // couple of places where we find this pattern, in which the async + // iterator is checking whether it's been disposed, so it'll know to + // stop iterating. + // + // IL_0024: ldarg.0 + // IL_0025: ldfld bool Test.AsyncEnumerableStateMachine/'d__0'::'<>w__disposeMode' + // IL_002a: brfalse.s IL_0031 + // + // We'll eliminate these wherever they appear. It's reasonable to just + // look for a "brfalse" or "brfalse.s" instruction, preceded immediately + // by "ldfld" of the compiler-generated "<>w__disposeMode" field. + + if (instruction.OpCode != OpCodes.Brfalse && + instruction.OpCode != OpCodes.Brfalse_S) + { + return false; + } - return false; - } + if (currentIndex >= 2 && + instructions[currentIndex - 1].OpCode == OpCodes.Ldfld && + ( + (instructions[currentIndex - 1].Operand is FieldDefinition field && IsCompilerGenerated(field) && field.FullName.EndsWith("__disposeMode")) || + (instructions[currentIndex - 1].Operand is FieldReference fieldRef && IsCompilerGenerated(fieldRef.Resolve()) && fieldRef.FullName.EndsWith("__disposeMode")) + ) && + (instructions[currentIndex - 2].OpCode == OpCodes.Ldarg || + instructions[currentIndex - 2].OpCode == OpCodes.Ldarg_0)) + { + return true; + } - static bool DisposeCheck(List instructions, Instruction instruction, int currentIndex) - { - // Within the compiler-generated async iterator, there are at least a - // couple of places where we find this pattern, in which the async - // iterator is checking whether it's been disposed, so it'll know to - // stop iterating. - // - // IL_0024: ldarg.0 - // IL_0025: ldfld bool Test.AsyncEnumerableStateMachine/'d__0'::'<>w__disposeMode' - // IL_002a: brfalse.s IL_0031 - // - // We'll eliminate these wherever they appear. It's reasonable to just - // look for a "brfalse" or "brfalse.s" instruction, preceded immediately - // by "ldfld" of the compiler-generated "<>w__disposeMode" field. - - if (instruction.OpCode != OpCodes.Brfalse && - instruction.OpCode != OpCodes.Brfalse_S) - { - return false; - } + return false; + } + } - if (currentIndex >= 2 && - instructions[currentIndex - 1].OpCode == OpCodes.Ldfld && - ( - (instructions[currentIndex - 1].Operand is FieldDefinition field && IsCompilerGenerated(field) && field.FullName.EndsWith("__disposeMode")) || - (instructions[currentIndex - 1].Operand is FieldReference fieldRef && IsCompilerGenerated(fieldRef.Resolve()) && fieldRef.FullName.EndsWith("__disposeMode")) - ) && - (instructions[currentIndex - 2].OpCode == OpCodes.Ldarg || - instructions[currentIndex - 2].OpCode == OpCodes.Ldarg_0)) - { - return true; - } + private static bool SkipGeneratedBranchesForEnumeratorCancellationAttribute(List instructions, Instruction instruction) + { + // For async-enumerable methods an additional cancellation token despite the default one can be passed. + // The EnumeratorCancellation attribute marks the parameter whose value is received by GetAsyncEnumerator(CancellationToken). + // Therefore the compiler generates the field x__combinedTokens and generates some additional branch points. + // + // IL_0118: ldarg.0 + // IL_0119: ldfld class [System.Runtime]System.Threading.CancellationTokenSource Issue1275.AwaitForeachReproduction/'d__1'::'<>x__combinedTokens' + // IL_011E: brfalse.s IL_0133 + // + // We'll eliminate these wherever they appear. It's reasonable to just look for a "brfalse" or "brfalse.s" instruction, preceded + // immediately by "ldfld" of the compiler-generated "<>x__combinedTokens" field. + + int branchIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); + + if (instruction.OpCode != OpCodes.Brfalse && + instruction.OpCode != OpCodes.Brfalse_S) + { + return false; + } + + if (branchIndex >= 2 && + instructions[branchIndex - 1].OpCode == OpCodes.Ldfld && + instructions[branchIndex - 1].Operand is FieldDefinition field && + field.FieldType.FullName.Equals("System.Threading.CancellationTokenSource") && + field.FullName.EndsWith("x__combinedTokens") && + (instructions[branchIndex - 2].OpCode == OpCodes.Ldarg || + instructions[branchIndex - 2].OpCode == OpCodes.Ldarg_0)) + { + return true; + } + return false; + } - return false; - } - } + // https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/Expression%20Breakpoints.md + private static bool SkipExpressionBreakpointsBranches(Instruction instruction) => instruction.Previous is not null && instruction.Previous.OpCode == OpCodes.Ldc_I4 && + instruction.Previous.Operand is int operandValue && operandValue == 1 && + instruction.Next is not null && instruction.Next.OpCode == OpCodes.Nop && + instruction.Operand == instruction.Next?.Next; - private static bool SkipGeneratedBranchesForEnumeratorCancellationAttribute(List instructions, Instruction instruction) + public IReadOnlyList GetBranchPoints(MethodDefinition methodDefinition) + { + var list = new List(); + if (methodDefinition is null) + { + return list; + } + + uint ordinal = 0; + var instructions = methodDefinition.Body.Instructions.ToList(); + + bool isAsyncStateMachineMoveNext = IsMoveNextInsideAsyncStateMachine(methodDefinition); + bool isMoveNextInsideAsyncStateMachineProlog = isAsyncStateMachineMoveNext && IsMoveNextInsideAsyncStateMachineProlog(methodDefinition); + bool isMoveNextInsideAsyncIterator = isAsyncStateMachineMoveNext && IsMoveNextInsideAsyncIterator(methodDefinition); + + // State machine for enumerator uses `brfalse.s`/`beq` or `switch` opcode depending on how many `yield` we have in the method body. + // For more than one `yield` a `switch` is emitted so we should only skip the first branch. In case of a single `yield` we need to + // skip the first two branches to avoid reporting a phantom branch. The first branch (`brfalse.s`) jumps to the `yield`ed value, + // the second one (`beq`) exits the enumeration. + bool skipFirstBranch = IsMoveNextInsideEnumerator(methodDefinition); + bool skipSecondBranch = false; + + foreach (Instruction instruction in instructions.Where(instruction => instruction.OpCode.FlowControl == FlowControl.Cond_Branch)) + { + try { - // For async-enumerable methods an additional cancellation token despite the default one can be passed. - // The EnumeratorCancellation attribute marks the parameter whose value is received by GetAsyncEnumerator(CancellationToken). - // Therefore the compiler generates the field x__combinedTokens and generates some additional branch points. - // - // IL_0118: ldarg.0 - // IL_0119: ldfld class [System.Runtime]System.Threading.CancellationTokenSource Issue1275.AwaitForeachReproduction/'d__1'::'<>x__combinedTokens' - // IL_011E: brfalse.s IL_0133 - // - // We'll eliminate these wherever they appear. It's reasonable to just look for a "brfalse" or "brfalse.s" instruction, preceded - // immediately by "ldfld" of the compiler-generated "<>x__combinedTokens" field. - - int branchIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); - - if (instruction.OpCode != OpCodes.Brfalse && - instruction.OpCode != OpCodes.Brfalse_S) + if (skipFirstBranch) + { + skipFirstBranch = false; + skipSecondBranch = instruction.OpCode.Code != Code.Switch; + continue; + } + + if (skipSecondBranch) + { + skipSecondBranch = false; + continue; + } + + if (isMoveNextInsideAsyncStateMachineProlog) + { + if (SkipMoveNextPrologueBranches(instruction) || SkipIsCompleteAwaiters(instruction)) { - return false; - } - - if (branchIndex >= 2 && - instructions[branchIndex - 1].OpCode == OpCodes.Ldfld && - instructions[branchIndex - 1].Operand is FieldDefinition field && - field.FieldType.FullName.Equals("System.Threading.CancellationTokenSource") && - field.FullName.EndsWith("x__combinedTokens") && - (instructions[branchIndex - 2].OpCode == OpCodes.Ldarg || - instructions[branchIndex - 2].OpCode == OpCodes.Ldarg_0)) - { - return true; + continue; } - return false; - } - - // https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/Expression%20Breakpoints.md - private static bool SkipExpressionBreakpointsBranches(Instruction instruction) => instruction.Previous is not null && instruction.Previous.OpCode == OpCodes.Ldc_I4 && - instruction.Previous.Operand is int operandValue && operandValue == 1 && - instruction.Next is not null && instruction.Next.OpCode == OpCodes.Nop && - instruction.Operand == instruction.Next?.Next; - - public IReadOnlyList GetBranchPoints(MethodDefinition methodDefinition) - { - var list = new List(); - if (methodDefinition is null) + } + + if (SkipExpressionBreakpointsBranches(instruction)) + { + continue; + } + + if (SkipLambdaCachedField(instruction)) + { + continue; + } + + if (isAsyncStateMachineMoveNext) + { + if (SkipGeneratedBranchesForExceptionHandlers(methodDefinition, instruction, instructions) || + SkipGeneratedBranchForExceptionRethrown(instructions, instruction) || + SkipGeneratedBranchesForAwaitForeach(instructions, instruction) || + SkipGeneratedBranchesForAwaitUsing(instructions, instruction)) { - return list; + continue; } + } - uint ordinal = 0; - var instructions = methodDefinition.Body.Instructions.ToList(); - - bool isAsyncStateMachineMoveNext = IsMoveNextInsideAsyncStateMachine(methodDefinition); - bool isMoveNextInsideAsyncStateMachineProlog = isAsyncStateMachineMoveNext && IsMoveNextInsideAsyncStateMachineProlog(methodDefinition); - bool isMoveNextInsideAsyncIterator = isAsyncStateMachineMoveNext && IsMoveNextInsideAsyncIterator(methodDefinition); - - // State machine for enumerator uses `brfalse.s`/`beq` or `switch` opcode depending on how many `yield` we have in the method body. - // For more than one `yield` a `switch` is emitted so we should only skip the first branch. In case of a single `yield` we need to - // skip the first two branches to avoid reporting a phantom branch. The first branch (`brfalse.s`) jumps to the `yield`ed value, - // the second one (`beq`) exits the enumeration. - bool skipFirstBranch = IsMoveNextInsideEnumerator(methodDefinition); - bool skipSecondBranch = false; - - foreach (Instruction instruction in instructions.Where(instruction => instruction.OpCode.FlowControl == FlowControl.Cond_Branch)) + if (isMoveNextInsideAsyncIterator) + { + if (SkipGeneratedBranchesForAsyncIterator(instructions, instruction)) { - try - { - if (skipFirstBranch) - { - skipFirstBranch = false; - skipSecondBranch = instruction.OpCode.Code != Code.Switch; - continue; - } - - if (skipSecondBranch) - { - skipSecondBranch = false; - continue; - } - - if (isMoveNextInsideAsyncStateMachineProlog) - { - if (SkipMoveNextPrologueBranches(instruction) || SkipIsCompleteAwaiters(instruction)) - { - continue; - } - } - - if (SkipExpressionBreakpointsBranches(instruction)) - { - continue; - } - - if (SkipLambdaCachedField(instruction)) - { - continue; - } - - if (isAsyncStateMachineMoveNext) - { - if (SkipGeneratedBranchesForExceptionHandlers(methodDefinition, instruction, instructions) || - SkipGeneratedBranchForExceptionRethrown(instructions, instruction) || - SkipGeneratedBranchesForAwaitForeach(instructions, instruction) || - SkipGeneratedBranchesForAwaitUsing(instructions, instruction)) - { - continue; - } - } - - if (isMoveNextInsideAsyncIterator) - { - if (SkipGeneratedBranchesForAsyncIterator(instructions, instruction)) - { - continue; - } - } - - if (SkipGeneratedBranchesForEnumeratorCancellationAttribute(instructions, instruction)) - { - continue; - } - - if (SkipBranchGeneratedExceptionFilter(instruction, methodDefinition)) - { - continue; - } - - if (SkipBranchGeneratedFinallyBlock(instruction, methodDefinition)) - { - continue; - } - - int pathCounter = 0; - - // store branch origin offset - int branchOffset = instruction.Offset; - SequencePoint closestSeqPt = FindClosestInstructionWithSequencePoint(methodDefinition.Body, instruction).Maybe(i => methodDefinition.DebugInformation.GetSequencePoint(i)); - int branchingInstructionLine = closestSeqPt.Maybe(x => x.StartLine, -1); - string document = closestSeqPt.Maybe(x => x.Document.Url); - - if (instruction.Next == null) - { - return list; - } - - if (!BuildPointsForConditionalBranch(list, instruction, branchingInstructionLine, document, branchOffset, pathCounter, instructions, ref ordinal, methodDefinition)) - { - return list; - } - } - catch (Exception) - { - continue; - } + continue; } - return list; - } + } - private static bool BuildPointsForConditionalBranch(List list, Instruction instruction, - int branchingInstructionLine, string document, int branchOffset, int pathCounter, - List instructions, ref uint ordinal, MethodDefinition methodDefinition) - { - // Add Default branch (Path=0) + if (SkipGeneratedBranchesForEnumeratorCancellationAttribute(instructions, instruction)) + { + continue; + } - // Follow else/default instruction - Instruction @else = instruction.Next; + if (SkipBranchGeneratedExceptionFilter(instruction, methodDefinition)) + { + continue; + } - List pathOffsetList = GetBranchPath(@else); + if (SkipBranchGeneratedFinallyBlock(instruction, methodDefinition)) + { + continue; + } - // add Path 0 - var path0 = new BranchPoint - { - StartLine = branchingInstructionLine, - Document = document, - Offset = branchOffset, - Ordinal = ordinal++, - Path = pathCounter++, - OffsetPoints = - pathOffsetList.Count > 1 - ? pathOffsetList.GetRange(0, pathOffsetList.Count - 1) - : new List(), - EndOffset = pathOffsetList.Last() - }; + int pathCounter = 0; - // Add Conditional Branch (Path=1) - if (instruction.OpCode.Code != Code.Switch) - { - // Follow instruction at operand - if (instruction.Operand is not Instruction @then) - return false; + // store branch origin offset + int branchOffset = instruction.Offset; + SequencePoint closestSeqPt = FindClosestInstructionWithSequencePoint(methodDefinition.Body, instruction).Maybe(i => methodDefinition.DebugInformation.GetSequencePoint(i)); + int branchingInstructionLine = closestSeqPt.Maybe(x => x.StartLine, -1); + string document = closestSeqPt.Maybe(x => x.Document.Url); - ordinal = BuildPointsForBranch(list, then, branchingInstructionLine, document, branchOffset, - ordinal, pathCounter, path0, instructions, methodDefinition); - } - else // instruction.OpCode.Code == Code.Switch - { - if (instruction.Operand is not Instruction[] branchInstructions || branchInstructions.Length == 0) - return false; + if (instruction.Next == null) + { + return list; + } - ordinal = BuildPointsForSwitchCases(list, path0, branchInstructions, branchingInstructionLine, - document, branchOffset, ordinal, ref pathCounter); - } - return true; + if (!BuildPointsForConditionalBranch(list, instruction, branchingInstructionLine, document, branchOffset, pathCounter, instructions, ref ordinal, methodDefinition)) + { + return list; + } } - - private static uint BuildPointsForBranch(List list, Instruction then, int branchingInstructionLine, string document, - int branchOffset, uint ordinal, int pathCounter, BranchPoint path0, List instructions, MethodDefinition methodDefinition) + catch (Exception) { - List pathOffsetList1 = GetBranchPath(@then); + continue; + } + } + return list; + } - // Add path 1 - var path1 = new BranchPoint - { - StartLine = branchingInstructionLine, - Document = document, - Offset = branchOffset, - Ordinal = ordinal++, - Path = pathCounter, - OffsetPoints = - pathOffsetList1.Count > 1 - ? pathOffsetList1.GetRange(0, pathOffsetList1.Count - 1) - : new List(), - EndOffset = pathOffsetList1.Last() - }; + private static bool BuildPointsForConditionalBranch(List list, Instruction instruction, + int branchingInstructionLine, string document, int branchOffset, int pathCounter, + List instructions, ref uint ordinal, MethodDefinition methodDefinition) + { + // Add Default branch (Path=0) + + // Follow else/default instruction + Instruction @else = instruction.Next; + + List pathOffsetList = GetBranchPath(@else); + + // add Path 0 + var path0 = new BranchPoint + { + StartLine = branchingInstructionLine, + Document = document, + Offset = branchOffset, + Ordinal = ordinal++, + Path = pathCounter++, + OffsetPoints = + pathOffsetList.Count > 1 + ? pathOffsetList.GetRange(0, pathOffsetList.Count - 1) + : new List(), + EndOffset = pathOffsetList.Last() + }; + + // Add Conditional Branch (Path=1) + if (instruction.OpCode.Code != Code.Switch) + { + // Follow instruction at operand + if (instruction.Operand is not Instruction @then) + return false; + + ordinal = BuildPointsForBranch(list, then, branchingInstructionLine, document, branchOffset, + ordinal, pathCounter, path0, instructions, methodDefinition); + } + else // instruction.OpCode.Code == Code.Switch + { + if (instruction.Operand is not Instruction[] branchInstructions || branchInstructions.Length == 0) + return false; + + ordinal = BuildPointsForSwitchCases(list, path0, branchInstructions, branchingInstructionLine, + document, branchOffset, ordinal, ref pathCounter); + } + return true; + } - // only add branch if branch does not match a known sequence - // e.g. auto generated field assignment - // or encapsulates at least one sequence point - int[] offsets = new[] - { + private static uint BuildPointsForBranch(List list, Instruction then, int branchingInstructionLine, string document, + int branchOffset, uint ordinal, int pathCounter, BranchPoint path0, List instructions, MethodDefinition methodDefinition) + { + List pathOffsetList1 = GetBranchPath(@then); + + // Add path 1 + var path1 = new BranchPoint + { + StartLine = branchingInstructionLine, + Document = document, + Offset = branchOffset, + Ordinal = ordinal++, + Path = pathCounter, + OffsetPoints = + pathOffsetList1.Count > 1 + ? pathOffsetList1.GetRange(0, pathOffsetList1.Count - 1) + : new List(), + EndOffset = pathOffsetList1.Last() + }; + + // only add branch if branch does not match a known sequence + // e.g. auto generated field assignment + // or encapsulates at least one sequence point + int[] offsets = new[] + { path0.Offset, path0.EndOffset, path1.Offset, path1.EndOffset }; - Code[][] ignoreSequences = new[] - { + Code[][] ignoreSequences = new[] + { // we may need other samples new[] {Code.Brtrue_S, Code.Pop, Code.Ldsfld, Code.Ldftn, Code.Newobj, Code.Dup, Code.Stsfld, Code.Newobj}, // CachedAnonymousMethodDelegate field allocation }; - int bs = offsets.Min(); - int be = offsets.Max(); + int bs = offsets.Min(); + int be = offsets.Max(); - var range = instructions.Where(i => (i.Offset >= bs) && (i.Offset <= be)).ToList(); + var range = instructions.Where(i => (i.Offset >= bs) && (i.Offset <= be)).ToList(); - bool match = ignoreSequences - .Where(ignoreSequence => range.Count >= ignoreSequence.Length) - .Any(ignoreSequence => range.Zip(ignoreSequence, (instruction, code) => instruction.OpCode.Code == code).All(x => x)); + bool match = ignoreSequences + .Where(ignoreSequence => range.Count >= ignoreSequence.Length) + .Any(ignoreSequence => range.Zip(ignoreSequence, (instruction, code) => instruction.OpCode.Code == code).All(x => x)); - int count = range - .Count(i => methodDefinition.DebugInformation.GetSequencePoint(i) != null); + int count = range + .Count(i => methodDefinition.DebugInformation.GetSequencePoint(i) != null); - if (!match || count > 0) - { - list.Add(path0); - list.Add(path1); - } - return ordinal; - } + if (!match || count > 0) + { + list.Add(path0); + list.Add(path1); + } + return ordinal; + } - private static uint BuildPointsForSwitchCases(List list, BranchPoint path0, Instruction[] branchInstructions, - int branchingInstructionLine, string document, int branchOffset, uint ordinal, ref int pathCounter) - { - int counter = pathCounter; - list.Add(path0); - // Add Conditional Branches (Path>0) - list.AddRange(branchInstructions.Select(GetBranchPath) - .Select(pathOffsetList1 => new BranchPoint - { - StartLine = branchingInstructionLine, - Document = document, - Offset = branchOffset, - Ordinal = ordinal++, - Path = counter++, - OffsetPoints = - pathOffsetList1.Count > 1 - ? pathOffsetList1.GetRange(0, pathOffsetList1.Count - 1) - : new List(), - EndOffset = pathOffsetList1.Last() - })); - pathCounter = counter; - return ordinal; - } + private static uint BuildPointsForSwitchCases(List list, BranchPoint path0, Instruction[] branchInstructions, + int branchingInstructionLine, string document, int branchOffset, uint ordinal, ref int pathCounter) + { + int counter = pathCounter; + list.Add(path0); + // Add Conditional Branches (Path>0) + list.AddRange(branchInstructions.Select(GetBranchPath) + .Select(pathOffsetList1 => new BranchPoint + { + StartLine = branchingInstructionLine, + Document = document, + Offset = branchOffset, + Ordinal = ordinal++, + Path = counter++, + OffsetPoints = + pathOffsetList1.Count > 1 + ? pathOffsetList1.GetRange(0, pathOffsetList1.Count - 1) + : new List(), + EndOffset = pathOffsetList1.Last() + })); + pathCounter = counter; + return ordinal; + } - public bool SkipNotCoverableInstruction(MethodDefinition methodDefinition, Instruction instruction) => - SkipNotCoverableInstructionAfterExceptionRethrowInsiceCatchBlock(methodDefinition, instruction) || - SkipExpressionBreakpointsSequences(methodDefinition, instruction); - - /* - Need to skip instrumentation after exception re-throw inside catch block (only for async state machine MoveNext()) - es: - try - { - ... - } - catch - { - await ... - throw; - } // need to skip instrumentation here - - We can detect this type of code block by searching for method ExceptionDispatchInfo.Throw() inside the compiled IL - ... - // ExceptionDispatchInfo.Capture(ex).Throw(); - IL_00c6: ldloc.s 6 - IL_00c8: call class [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Capture(class [System.Runtime]System.Exception) - IL_00cd: callvirt instance void [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Throw() - // NOT COVERABLE - IL_00d2: nop - IL_00d3: nop - ... + public bool SkipNotCoverableInstruction(MethodDefinition methodDefinition, Instruction instruction) => + SkipNotCoverableInstructionAfterExceptionRethrowInsiceCatchBlock(methodDefinition, instruction) || + SkipExpressionBreakpointsSequences(methodDefinition, instruction); - In case of nested code blocks inside catch we need to detect also goto calls - ... - // ExceptionDispatchInfo.Capture(ex).Throw(); - IL_00d3: ldloc.s 7 - IL_00d5: call class [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Capture(class [System.Runtime]System.Exception) - IL_00da: callvirt instance void [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Throw() - // NOT COVERABLE - IL_00df: nop - IL_00e0: nop - IL_00e1: br.s IL_00ea + /* + Need to skip instrumentation after exception re-throw inside catch block (only for async state machine MoveNext()) + es: + try + { ... - // NOT COVERABLE - IL_00ea: nop - IL_00eb: br.s IL_00ed - ... - */ - public bool SkipNotCoverableInstructionAfterExceptionRethrowInsiceCatchBlock(MethodDefinition methodDefinition, Instruction instruction) + } + catch + { + await ... + throw; + } // need to skip instrumentation here + + We can detect this type of code block by searching for method ExceptionDispatchInfo.Throw() inside the compiled IL + ... + // ExceptionDispatchInfo.Capture(ex).Throw(); + IL_00c6: ldloc.s 6 + IL_00c8: call class [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Capture(class [System.Runtime]System.Exception) + IL_00cd: callvirt instance void [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Throw() + // NOT COVERABLE + IL_00d2: nop + IL_00d3: nop + ... + + In case of nested code blocks inside catch we need to detect also goto calls + ... + // ExceptionDispatchInfo.Capture(ex).Throw(); + IL_00d3: ldloc.s 7 + IL_00d5: call class [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Capture(class [System.Runtime]System.Exception) + IL_00da: callvirt instance void [System.Runtime]System.Runtime.ExceptionServices.ExceptionDispatchInfo::Throw() + // NOT COVERABLE + IL_00df: nop + IL_00e0: nop + IL_00e1: br.s IL_00ea + ... + // NOT COVERABLE + IL_00ea: nop + IL_00eb: br.s IL_00ed + ... + */ + public bool SkipNotCoverableInstructionAfterExceptionRethrowInsiceCatchBlock(MethodDefinition methodDefinition, Instruction instruction) + { + if (!IsMoveNextInsideAsyncStateMachine(methodDefinition)) + { + return false; + } + + if (instruction.OpCode != OpCodes.Nop) + { + return false; + } + + // detect if current instruction is not coverable + Instruction prev = GetPreviousNoNopInstruction(instruction); + if (prev != null && + prev.OpCode == OpCodes.Callvirt && + prev.Operand is MethodReference mr && mr.FullName == "System.Void System.Runtime.ExceptionServices.ExceptionDispatchInfo::Throw()") + { + return true; + } + + // find the caller of current instruction and detect if not coverable + prev = instruction.Previous; + while (prev != null) + { + if (prev.Operand is Instruction i && (i.Offset == instruction.Offset || i.Offset == prev.Next.Offset)) // caller { - if (!IsMoveNextInsideAsyncStateMachine(methodDefinition)) - { - return false; - } - - if (instruction.OpCode != OpCodes.Nop) - { - return false; - } - - // detect if current instruction is not coverable - Instruction prev = GetPreviousNoNopInstruction(instruction); - if (prev != null && - prev.OpCode == OpCodes.Callvirt && - prev.Operand is MethodReference mr && mr.FullName == "System.Void System.Runtime.ExceptionServices.ExceptionDispatchInfo::Throw()") - { - return true; - } - - // find the caller of current instruction and detect if not coverable - prev = instruction.Previous; - while (prev != null) - { - if (prev.Operand is Instruction i && (i.Offset == instruction.Offset || i.Offset == prev.Next.Offset)) // caller - { - prev = GetPreviousNoNopInstruction(prev); - break; - } - prev = prev.Previous; - } - - return prev != null && - prev.OpCode == OpCodes.Callvirt && - prev.Operand is MethodReference mr1 && mr1.FullName == "System.Void System.Runtime.ExceptionServices.ExceptionDispatchInfo::Throw()"; - - // local helper - static Instruction GetPreviousNoNopInstruction(Instruction i) - { - Instruction instruction = i.Previous; - while (instruction != null) - { - if (instruction.OpCode != OpCodes.Nop) - { - return instruction; - } - instruction = instruction.Previous; - } - - return null; - } + prev = GetPreviousNoNopInstruction(prev); + break; } - - private bool SkipExpressionBreakpointsSequences(MethodDefinition methodDefinition, Instruction instruction) + prev = prev.Previous; + } + + return prev != null && + prev.OpCode == OpCodes.Callvirt && + prev.Operand is MethodReference mr1 && mr1.FullName == "System.Void System.Runtime.ExceptionServices.ExceptionDispatchInfo::Throw()"; + + // local helper + static Instruction GetPreviousNoNopInstruction(Instruction i) + { + Instruction instruction = i.Previous; + while (instruction != null) { - if (_sequencePointOffsetToSkip.ContainsKey(methodDefinition.FullName) && _sequencePointOffsetToSkip[methodDefinition.FullName].Contains(instruction.Offset) && instruction.OpCode == OpCodes.Nop) - { - return true; - } - /* - Sequence to skip https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/Expression%20Breakpoints.md - // if (1 == 0) - // sequence point: (line 33, col 9) to (line 40, col 10) in C:\git\coverletfork\test\coverlet.core.tests\Samples\Instrumentation.SelectionStatements.cs - IL_0000: ldc.i4.1 - IL_0001: brtrue.s IL_0004 - // if (value is int) - // sequence point: (line 34, col 9) to (line 40, col 10) in C:\git\coverletfork\test\coverlet.core.tests\Samples\Instrumentation.SelectionStatements.cs - IL_0003: nop - // sequence point: hidden - ... - */ - if ( - instruction.OpCode == OpCodes.Ldc_I4 && instruction.Operand is int operandValue && operandValue == 1 && - instruction.Next?.OpCode == OpCodes.Brtrue && - instruction.Next?.Next?.OpCode == OpCodes.Nop && - instruction.Next?.Operand == instruction.Next?.Next?.Next && - methodDefinition.DebugInformation.GetSequencePoint(instruction.Next?.Next) is not null - ) - { - if (!_sequencePointOffsetToSkip.ContainsKey(methodDefinition.FullName)) - { - _sequencePointOffsetToSkip.TryAdd(methodDefinition.FullName, new List()); - } - _sequencePointOffsetToSkip[methodDefinition.FullName].Add(instruction.Offset); - _sequencePointOffsetToSkip[methodDefinition.FullName].Add(instruction.Next.Offset); - _sequencePointOffsetToSkip[methodDefinition.FullName].Add(instruction.Next.Next.Offset); - } - - return false; + if (instruction.OpCode != OpCodes.Nop) + { + return instruction; + } + instruction = instruction.Previous; } - public bool SkipInlineAssignedAutoProperty(bool skipAutoProps, MethodDefinition methodDefinition, Instruction instruction) - { - if (!skipAutoProps || !methodDefinition.IsConstructor) return false; - - return SkipGeneratedBackingFieldAssignment(methodDefinition, instruction) || - SkipDefaultInitializationSystemObject(instruction); - } + return null; + } + } - private static bool SkipGeneratedBackingFieldAssignment(MethodDefinition methodDefinition, Instruction instruction) + private bool SkipExpressionBreakpointsSequences(MethodDefinition methodDefinition, Instruction instruction) + { + if (_sequencePointOffsetToSkip.ContainsKey(methodDefinition.FullName) && _sequencePointOffsetToSkip[methodDefinition.FullName].Contains(instruction.Offset) && instruction.OpCode == OpCodes.Nop) + { + return true; + } + /* + Sequence to skip https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/Expression%20Breakpoints.md + // if (1 == 0) + // sequence point: (line 33, col 9) to (line 40, col 10) in C:\git\coverletfork\test\coverlet.core.tests\Samples\Instrumentation.SelectionStatements.cs + IL_0000: ldc.i4.1 + IL_0001: brtrue.s IL_0004 + // if (value is int) + // sequence point: (line 34, col 9) to (line 40, col 10) in C:\git\coverletfork\test\coverlet.core.tests\Samples\Instrumentation.SelectionStatements.cs + IL_0003: nop + // sequence point: hidden + ... + */ + if ( + instruction.OpCode == OpCodes.Ldc_I4 && instruction.Operand is int operandValue && operandValue == 1 && + instruction.Next?.OpCode == OpCodes.Brtrue && + instruction.Next?.Next?.OpCode == OpCodes.Nop && + instruction.Next?.Operand == instruction.Next?.Next?.Next && + methodDefinition.DebugInformation.GetSequencePoint(instruction.Next?.Next) is not null + ) + { + if (!_sequencePointOffsetToSkip.ContainsKey(methodDefinition.FullName)) { - /* - For inline initialization of properties the compiler generates a field that is set in the constructor of the class. - To skip this we search for compiler generated fields that are set in the constructor. - - .field private string 'k__BackingField' - .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( - 01 00 00 00 - ) - - .method public hidebysig specialname rtspecialname - instance void .ctor () cil managed - { - IL_0000: ldarg.0 - IL_0001: ldsfld string[System.Runtime] System.String::Empty - IL_0006: stfld string TestRepro.ClassWithPropertyInit::'k__BackingField' - ... - } - ... - */ - IEnumerable autogeneratedBackingFields = methodDefinition.DeclaringType.Fields.Where(x => - x.CustomAttributes.Any(ca => ca.AttributeType.FullName.Equals(typeof(CompilerGeneratedAttribute).FullName)) && - x.FullName.EndsWith("k__BackingField")); - - return instruction.OpCode == OpCodes.Ldarg && - instruction.Next?.Next?.OpCode == OpCodes.Stfld && - instruction.Next?.Next?.Operand is FieldReference fr && - autogeneratedBackingFields.Select(x => x.FullName).Contains(fr.FullName); + _sequencePointOffsetToSkip.TryAdd(methodDefinition.FullName, new List()); } + _sequencePointOffsetToSkip[methodDefinition.FullName].Add(instruction.Offset); + _sequencePointOffsetToSkip[methodDefinition.FullName].Add(instruction.Next.Offset); + _sequencePointOffsetToSkip[methodDefinition.FullName].Add(instruction.Next.Next.Offset); + } - private static bool SkipDefaultInitializationSystemObject(Instruction instruction) - { - /* - A type always has a constructor with a default instantiation of System.Object. For record types these - instructions can have a own sequence point. This means that even the default constructor would be instrumented. - To skip this we search for call instructions with a method reference that declares System.Object. - - IL_0000: ldarg.0 - IL_0001: call instance void [System.Runtime]System.Object::.ctor() - IL_0006: ret - */ - return instruction.OpCode == OpCodes.Ldarg && - instruction.Next?.OpCode == OpCodes.Call && - instruction.Next?.Operand is MethodReference mr && mr.DeclaringType.FullName.Equals(typeof(System.Object).FullName); - } + return false; + } - private static bool SkipBranchGeneratedExceptionFilter(Instruction branchInstruction, MethodDefinition methodDefinition) - { - if (!methodDefinition.Body.HasExceptionHandlers) - return false; + public bool SkipInlineAssignedAutoProperty(bool skipAutoProps, MethodDefinition methodDefinition, Instruction instruction) + { + if (!skipAutoProps || !methodDefinition.IsConstructor) return false; - // a generated filter block will have no sequence points in its range - var handlers = methodDefinition.Body.ExceptionHandlers - .Where(e => e.HandlerType == ExceptionHandlerType.Filter) - .ToList(); + return SkipGeneratedBackingFieldAssignment(methodDefinition, instruction) || + SkipDefaultInitializationSystemObject(instruction); + } - foreach (ExceptionHandler exceptionHandler in handlers) - { - Instruction startFilter = exceptionHandler.FilterStart; - Instruction endFilter = startFilter; + private static bool SkipGeneratedBackingFieldAssignment(MethodDefinition methodDefinition, Instruction instruction) + { + /* + For inline initialization of properties the compiler generates a field that is set in the constructor of the class. + To skip this we search for compiler generated fields that are set in the constructor. + + .field private string 'k__BackingField' + .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + + .method public hidebysig specialname rtspecialname + instance void .ctor () cil managed + { + IL_0000: ldarg.0 + IL_0001: ldsfld string[System.Runtime] System.String::Empty + IL_0006: stfld string TestRepro.ClassWithPropertyInit::'k__BackingField' + ... + } + ... + */ + IEnumerable autogeneratedBackingFields = methodDefinition.DeclaringType.Fields.Where(x => + x.CustomAttributes.Any(ca => ca.AttributeType.FullName.Equals(typeof(CompilerGeneratedAttribute).FullName)) && + x.FullName.EndsWith("k__BackingField")); + + return instruction.OpCode == OpCodes.Ldarg && + instruction.Next?.Next?.OpCode == OpCodes.Stfld && + instruction.Next?.Next?.Operand is FieldReference fr && + autogeneratedBackingFields.Select(x => x.FullName).Contains(fr.FullName); + } - while (endFilter != null && endFilter.OpCode != OpCodes.Endfilter) - { - endFilter = endFilter.Next; - } + private static bool SkipDefaultInitializationSystemObject(Instruction instruction) + { + /* + A type always has a constructor with a default instantiation of System.Object. For record types these + instructions can have a own sequence point. This means that even the default constructor would be instrumented. + To skip this we search for call instructions with a method reference that declares System.Object. + + IL_0000: ldarg.0 + IL_0001: call instance void [System.Runtime]System.Object::.ctor() + IL_0006: ret + */ + return instruction.OpCode == OpCodes.Ldarg && + instruction.Next?.OpCode == OpCodes.Call && + instruction.Next?.Operand is MethodReference mr && mr.DeclaringType.FullName.Equals(typeof(System.Object).FullName); + } - if (branchInstruction.Offset >= startFilter.Offset && branchInstruction.Offset <= endFilter.Offset) - { - return true; - } - } + private static bool SkipBranchGeneratedExceptionFilter(Instruction branchInstruction, MethodDefinition methodDefinition) + { + if (!methodDefinition.Body.HasExceptionHandlers) + return false; - return false; - } + // a generated filter block will have no sequence points in its range + var handlers = methodDefinition.Body.ExceptionHandlers + .Where(e => e.HandlerType == ExceptionHandlerType.Filter) + .ToList(); - private static bool SkipBranchGeneratedFinallyBlock(Instruction branchInstruction, MethodDefinition methodDefinition) - { - if (!methodDefinition.Body.HasExceptionHandlers) - return false; - - // a generated finally block will have no sequence points in its range - var handlers = methodDefinition.Body.ExceptionHandlers - .Where(e => e.HandlerType == ExceptionHandlerType.Finally) - .ToList(); - - return handlers - .Where(e => branchInstruction.Offset >= e.HandlerStart.Offset) - .Where(e => branchInstruction.Offset < e.HandlerEnd.Maybe(h => h.Offset, GetOffsetOfNextEndfinally(methodDefinition.Body, e.HandlerStart.Offset))) - .OrderByDescending(h => h.HandlerStart.Offset) // we need to work inside out - .Any(eh => !(methodDefinition.DebugInformation.GetSequencePointMapping() - .Where(i => i.Value.StartLine != StepOverLineCode) - .Any(i => i.Value.Offset >= eh.HandlerStart.Offset && i.Value.Offset < eh.HandlerEnd.Maybe(h => h.Offset, GetOffsetOfNextEndfinally(methodDefinition.Body, eh.HandlerStart.Offset))))); - } + foreach (ExceptionHandler exceptionHandler in handlers) + { + Instruction startFilter = exceptionHandler.FilterStart; + Instruction endFilter = startFilter; - private static int GetOffsetOfNextEndfinally(MethodBody body, int startOffset) + while (endFilter != null && endFilter.OpCode != OpCodes.Endfilter) { - int lastOffset = body.Instructions.LastOrDefault().Maybe(i => i.Offset, int.MaxValue); - return body.Instructions.FirstOrDefault(i => i.Offset >= startOffset && i.OpCode.Code == Code.Endfinally).Maybe(i => i.Offset, lastOffset); + endFilter = endFilter.Next; } - private static List GetBranchPath(Instruction instruction) + if (branchInstruction.Offset >= startFilter.Offset && branchInstruction.Offset <= endFilter.Offset) { - var offsetList = new List(); + return true; + } + } - if (instruction != null) - { - Instruction point = instruction; - offsetList.Add(point.Offset); - while (point.OpCode == OpCodes.Br || point.OpCode == OpCodes.Br_S) - { - if (point.Operand is Instruction nextPoint) - { - point = nextPoint; - offsetList.Add(point.Offset); - } - else - { - break; - } - } - } + return false; + } - return offsetList; - } + private static bool SkipBranchGeneratedFinallyBlock(Instruction branchInstruction, MethodDefinition methodDefinition) + { + if (!methodDefinition.Body.HasExceptionHandlers) + return false; + + // a generated finally block will have no sequence points in its range + var handlers = methodDefinition.Body.ExceptionHandlers + .Where(e => e.HandlerType == ExceptionHandlerType.Finally) + .ToList(); + + return handlers + .Where(e => branchInstruction.Offset >= e.HandlerStart.Offset) + .Where(e => branchInstruction.Offset < e.HandlerEnd.Maybe(h => h.Offset, GetOffsetOfNextEndfinally(methodDefinition.Body, e.HandlerStart.Offset))) + .OrderByDescending(h => h.HandlerStart.Offset) // we need to work inside out + .Any(eh => !(methodDefinition.DebugInformation.GetSequencePointMapping() + .Where(i => i.Value.StartLine != StepOverLineCode) + .Any(i => i.Value.Offset >= eh.HandlerStart.Offset && i.Value.Offset < eh.HandlerEnd.Maybe(h => h.Offset, GetOffsetOfNextEndfinally(methodDefinition.Body, eh.HandlerStart.Offset))))); + } - private static Instruction FindClosestInstructionWithSequencePoint(MethodBody methodBody, Instruction instruction) - { - var sequencePointsInMethod = methodBody.Instructions.Where(i => HasValidSequencePoint(i, methodBody.Method)).ToList(); - if (!sequencePointsInMethod.Any()) - return null; - int idx = sequencePointsInMethod.BinarySearch(instruction, new InstructionByOffsetComparer()); - Instruction prev; - if (idx < 0) - { - // no exact match, idx corresponds to the next, larger element - int lower = Math.Max(~idx - 1, 0); - prev = sequencePointsInMethod[lower]; - } - else - { - // exact match, idx corresponds to the match - prev = sequencePointsInMethod[idx]; - } + private static int GetOffsetOfNextEndfinally(MethodBody body, int startOffset) + { + int lastOffset = body.Instructions.LastOrDefault().Maybe(i => i.Offset, int.MaxValue); + return body.Instructions.FirstOrDefault(i => i.Offset >= startOffset && i.OpCode.Code == Code.Endfinally).Maybe(i => i.Offset, lastOffset); + } - return prev; - } + private static List GetBranchPath(Instruction instruction) + { + var offsetList = new List(); - private static bool HasValidSequencePoint(Instruction instruction, MethodDefinition methodDefinition) + if (instruction != null) + { + Instruction point = instruction; + offsetList.Add(point.Offset); + while (point.OpCode == OpCodes.Br || point.OpCode == OpCodes.Br_S) { - SequencePoint sp = methodDefinition.DebugInformation.GetSequencePoint(instruction); - return sp != null && sp.StartLine != StepOverLineCode; + if (point.Operand is Instruction nextPoint) + { + point = nextPoint; + offsetList.Add(point.Offset); + } + else + { + break; + } } + } - private class InstructionByOffsetComparer : IComparer - { - public int Compare(Instruction x, Instruction y) - { - return x.Offset.CompareTo(y.Offset); - } - } + return offsetList; + } + + private static Instruction FindClosestInstructionWithSequencePoint(MethodBody methodBody, Instruction instruction) + { + var sequencePointsInMethod = methodBody.Instructions.Where(i => HasValidSequencePoint(i, methodBody.Method)).ToList(); + if (!sequencePointsInMethod.Any()) + return null; + int idx = sequencePointsInMethod.BinarySearch(instruction, new InstructionByOffsetComparer()); + Instruction prev; + if (idx < 0) + { + // no exact match, idx corresponds to the next, larger element + int lower = Math.Max(~idx - 1, 0); + prev = sequencePointsInMethod[lower]; + } + else + { + // exact match, idx corresponds to the match + prev = sequencePointsInMethod[idx]; + } + + return prev; + } + + private static bool HasValidSequencePoint(Instruction instruction, MethodDefinition methodDefinition) + { + SequencePoint sp = methodDefinition.DebugInformation.GetSequencePoint(instruction); + return sp != null && sp.StartLine != StepOverLineCode; + } + + private class InstructionByOffsetComparer : IComparer + { + public int Compare(Instruction x, Instruction y) + { + return x.Offset.CompareTo(y.Offset); + } } + } } diff --git a/src/coverlet.msbuild.tasks/BaseTask.cs b/src/coverlet.msbuild.tasks/BaseTask.cs index 783b990bb..1798b95b4 100644 --- a/src/coverlet.msbuild.tasks/BaseTask.cs +++ b/src/coverlet.msbuild.tasks/BaseTask.cs @@ -6,8 +6,8 @@ namespace Coverlet.MSbuild.Tasks { - public abstract class BaseTask : Task - { - public static IServiceProvider ServiceProvider { get; protected internal set; } - } + public abstract class BaseTask : Task + { + public static IServiceProvider ServiceProvider { get; protected internal set; } + } } diff --git a/src/coverlet.msbuild.tasks/ReportWriter.cs b/src/coverlet.msbuild.tasks/ReportWriter.cs index c0de6bd5e..65225f098 100644 --- a/src/coverlet.msbuild.tasks/ReportWriter.cs +++ b/src/coverlet.msbuild.tasks/ReportWriter.cs @@ -7,49 +7,49 @@ namespace Coverlet.MSbuild.Tasks { - internal class ReportWriter - { - private readonly string _coverletMultiTargetFrameworksCurrentTFM; - private readonly string _directory; - private readonly string _output; - private readonly IReporter _reporter; - private readonly IFileSystem _fileSystem; - private readonly ISourceRootTranslator _sourceRootTranslator; - private readonly CoverageResult _result; + internal class ReportWriter + { + private readonly string _coverletMultiTargetFrameworksCurrentTFM; + private readonly string _directory; + private readonly string _output; + private readonly IReporter _reporter; + private readonly IFileSystem _fileSystem; + private readonly ISourceRootTranslator _sourceRootTranslator; + private readonly CoverageResult _result; - public ReportWriter(string coverletMultiTargetFrameworksCurrentTFM, string directory, string output, - IReporter reporter, IFileSystem fileSystem, CoverageResult result, ISourceRootTranslator sourceRootTranslator) - => (_coverletMultiTargetFrameworksCurrentTFM, _directory, _output, _reporter, _fileSystem, _result, _sourceRootTranslator) = - (coverletMultiTargetFrameworksCurrentTFM, directory, output, reporter, fileSystem, result, sourceRootTranslator); + public ReportWriter(string coverletMultiTargetFrameworksCurrentTFM, string directory, string output, + IReporter reporter, IFileSystem fileSystem, CoverageResult result, ISourceRootTranslator sourceRootTranslator) + => (_coverletMultiTargetFrameworksCurrentTFM, _directory, _output, _reporter, _fileSystem, _result, _sourceRootTranslator) = + (coverletMultiTargetFrameworksCurrentTFM, directory, output, reporter, fileSystem, result, sourceRootTranslator); - public string WriteReport() - { - string filename = Path.GetFileName(_output); + public string WriteReport() + { + string filename = Path.GetFileName(_output); - string separatorPoint = string.IsNullOrEmpty(_coverletMultiTargetFrameworksCurrentTFM) ? "" : "."; + string separatorPoint = string.IsNullOrEmpty(_coverletMultiTargetFrameworksCurrentTFM) ? "" : "."; - if (filename == string.Empty) - { - // empty filename for instance only directory is passed to CoverletOutput c:\reportpath - // c:\reportpath\coverage.reportedextension - filename = $"coverage.{_coverletMultiTargetFrameworksCurrentTFM}{separatorPoint}{_reporter.Extension}"; - } - else if (Path.HasExtension(filename)) - { - // filename with extension for instance c:\reportpath\file.ext - // we keep user specified name - filename = $"{Path.GetFileNameWithoutExtension(filename)}{separatorPoint}{_coverletMultiTargetFrameworksCurrentTFM}{Path.GetExtension(filename)}"; - } - else - { - // filename without extension for instance c:\reportpath\file - // c:\reportpath\file.reportedextension - filename = $"{filename}{separatorPoint}{_coverletMultiTargetFrameworksCurrentTFM}.{_reporter.Extension}"; - } + if (filename == string.Empty) + { + // empty filename for instance only directory is passed to CoverletOutput c:\reportpath + // c:\reportpath\coverage.reportedextension + filename = $"coverage.{_coverletMultiTargetFrameworksCurrentTFM}{separatorPoint}{_reporter.Extension}"; + } + else if (Path.HasExtension(filename)) + { + // filename with extension for instance c:\reportpath\file.ext + // we keep user specified name + filename = $"{Path.GetFileNameWithoutExtension(filename)}{separatorPoint}{_coverletMultiTargetFrameworksCurrentTFM}{Path.GetExtension(filename)}"; + } + else + { + // filename without extension for instance c:\reportpath\file + // c:\reportpath\file.reportedextension + filename = $"{filename}{separatorPoint}{_coverletMultiTargetFrameworksCurrentTFM}.{_reporter.Extension}"; + } - string report = Path.Combine(_directory, filename); - _fileSystem.WriteAllText(report, _reporter.Report(_result, _sourceRootTranslator)); - return report; - } + string report = Path.Combine(_directory, filename); + _fileSystem.WriteAllText(report, _reporter.Report(_result, _sourceRootTranslator)); + return report; } + } } diff --git a/test/coverlet.collector.tests/AttachmentManagerTests.cs b/test/coverlet.collector.tests/AttachmentManagerTests.cs index 9e021212d..bfa5d5f82 100644 --- a/test/coverlet.collector.tests/AttachmentManagerTests.cs +++ b/test/coverlet.collector.tests/AttachmentManagerTests.cs @@ -4,118 +4,118 @@ using System; using System.ComponentModel; using System.IO; +using Coverlet.Collector.DataCollection; +using Coverlet.Collector.Utilities; +using Coverlet.Collector.Utilities.Interfaces; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; -using Xunit; using Moq; -using Coverlet.Collector.Utilities; -using Coverlet.Collector.Utilities.Interfaces; -using Coverlet.Collector.DataCollection; +using Xunit; namespace Coverlet.Collector.Tests { - public class AttachmentManagerTests + public class AttachmentManagerTests + { + private AttachmentManager _attachmentManager; + private readonly Mock _mockDataCollectionSink; + private readonly DataCollectionContext _dataCollectionContext; + private readonly TestPlatformLogger _testPlatformLogger; + private readonly TestPlatformEqtTrace _eqtTrace; + private readonly Mock _mockFileHelper; + private readonly Mock _mockDirectoryHelper; + private readonly Mock _mockCountDownEvent; + private readonly Mock _mockDataCollectionLogger; + + public AttachmentManagerTests() + { + _mockDataCollectionSink = new Mock(); + _mockDataCollectionLogger = new Mock(); + var testcase = new TestCase { Id = Guid.NewGuid() }; + _dataCollectionContext = new DataCollectionContext(testcase); + _testPlatformLogger = new TestPlatformLogger(_mockDataCollectionLogger.Object, _dataCollectionContext); + _eqtTrace = new TestPlatformEqtTrace(); + _mockFileHelper = new Mock(); + _mockDirectoryHelper = new Mock(); + _mockCountDownEvent = new Mock(); + + _attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger, + _eqtTrace, @"E:\temp", _mockFileHelper.Object, _mockDirectoryHelper.Object, _mockCountDownEvent.Object); + } + + [Fact] + public void SendCoverageReportShouldSaveReportToFile() + { + string coverageReport = "" + + "" + + "" + + "" + + ""; + + _attachmentManager.SendCoverageReport(coverageReport, "report.cobertura.xml"); + _mockFileHelper.Verify(x => x.WriteAllText(It.Is(y => y.Contains(@"report.cobertura.xml")), coverageReport), Times.Once); + } + + [Fact] + public void SendCoverageReportShouldThrowExceptionWhenFailedToSaveReportToFile() + { + _attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger, + _eqtTrace, @"E:\temp", _mockFileHelper.Object, _mockDirectoryHelper.Object, _mockCountDownEvent.Object); + + string coverageReport = "" + + "" + + "" + + "" + + ""; + + string message = Assert.Throws(() => _attachmentManager.SendCoverageReport(coverageReport, null)).Message; + Assert.Contains("CoverletCoverageDataCollector: Failed to save coverage report", message); + } + + [Fact] + public void SendCoverageReportShouldSendAttachmentToTestPlatform() + { + DirectoryInfo directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + _attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger, + _eqtTrace, directory.ToString(), new FileHelper(), _mockDirectoryHelper.Object, _mockCountDownEvent.Object); + + string coverageReport = "" + + "" + + "" + + "" + + ""; + + _attachmentManager.SendCoverageReport(coverageReport, "report.cobertura.xml"); + + _mockDataCollectionSink.Verify(x => x.SendFileAsync(It.IsAny())); + + directory.Delete(true); + } + + [Fact] + public void OnDisposeAttachmentManagerShouldCleanUpReportDirectory() + { + var mockDirectoryHelper = new Mock(); + mockDirectoryHelper.Setup(x => x.Exists(It.Is(y => y.Contains(@"E:\temp")))).Returns(true); + using (var attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger, _eqtTrace, @"E:\temp", _mockFileHelper.Object, mockDirectoryHelper.Object, _mockCountDownEvent.Object)) + { + _mockDataCollectionSink.Raise(x => x.SendFileCompleted += null, new AsyncCompletedEventArgs(null, false, null)); + } + + mockDirectoryHelper.Verify(x => x.Delete(It.Is(y => y.Contains(@"E:\temp")), true), Times.Once); + } + + [Fact] + public void OnDisposeAttachmentManagerShouldThrowCoverletDataCollectorExceptionIfUnableToCleanUpReportDirectory() { - private AttachmentManager _attachmentManager; - private readonly Mock _mockDataCollectionSink; - private readonly DataCollectionContext _dataCollectionContext; - private readonly TestPlatformLogger _testPlatformLogger; - private readonly TestPlatformEqtTrace _eqtTrace; - private readonly Mock _mockFileHelper; - private readonly Mock _mockDirectoryHelper; - private readonly Mock _mockCountDownEvent; - private readonly Mock _mockDataCollectionLogger; - - public AttachmentManagerTests() - { - _mockDataCollectionSink = new Mock(); - _mockDataCollectionLogger = new Mock(); - var testcase = new TestCase { Id = Guid.NewGuid() }; - _dataCollectionContext = new DataCollectionContext(testcase); - _testPlatformLogger = new TestPlatformLogger(_mockDataCollectionLogger.Object, _dataCollectionContext); - _eqtTrace = new TestPlatformEqtTrace(); - _mockFileHelper = new Mock(); - _mockDirectoryHelper = new Mock(); - _mockCountDownEvent = new Mock(); - - _attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger, - _eqtTrace, @"E:\temp", _mockFileHelper.Object, _mockDirectoryHelper.Object, _mockCountDownEvent.Object); - } - - [Fact] - public void SendCoverageReportShouldSaveReportToFile() - { - string coverageReport = "" - + "" - + "" - + "" - + ""; - - _attachmentManager.SendCoverageReport(coverageReport, "report.cobertura.xml"); - _mockFileHelper.Verify(x => x.WriteAllText(It.Is(y => y.Contains(@"report.cobertura.xml")), coverageReport), Times.Once); - } - - [Fact] - public void SendCoverageReportShouldThrowExceptionWhenFailedToSaveReportToFile() - { - _attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger, - _eqtTrace, @"E:\temp", _mockFileHelper.Object, _mockDirectoryHelper.Object, _mockCountDownEvent.Object); - - string coverageReport = "" - + "" - + "" - + "" - + ""; - - string message = Assert.Throws(() => _attachmentManager.SendCoverageReport(coverageReport, null)).Message; - Assert.Contains("CoverletCoverageDataCollector: Failed to save coverage report", message); - } - - [Fact] - public void SendCoverageReportShouldSendAttachmentToTestPlatform() - { - DirectoryInfo directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); - _attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger, - _eqtTrace, directory.ToString(), new FileHelper(), _mockDirectoryHelper.Object, _mockCountDownEvent.Object); - - string coverageReport = "" - + "" - + "" - + "" - + ""; - - _attachmentManager.SendCoverageReport(coverageReport, "report.cobertura.xml"); - - _mockDataCollectionSink.Verify(x => x.SendFileAsync(It.IsAny())); - - directory.Delete(true); - } - - [Fact] - public void OnDisposeAttachmentManagerShouldCleanUpReportDirectory() - { - var mockDirectoryHelper = new Mock(); - mockDirectoryHelper.Setup(x => x.Exists(It.Is(y => y.Contains(@"E:\temp")))).Returns(true); - using (var attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger, _eqtTrace, @"E:\temp", _mockFileHelper.Object, mockDirectoryHelper.Object, _mockCountDownEvent.Object)) - { - _mockDataCollectionSink.Raise(x => x.SendFileCompleted += null, new AsyncCompletedEventArgs(null, false, null)); - } - - mockDirectoryHelper.Verify(x => x.Delete(It.Is(y => y.Contains(@"E:\temp")), true), Times.Once); - } - - [Fact] - public void OnDisposeAttachmentManagerShouldThrowCoverletDataCollectorExceptionIfUnableToCleanUpReportDirectory() - { - var mockDirectoryHelper = new Mock(); - mockDirectoryHelper.Setup(x => x.Exists(It.Is(y => y.Contains(@"E:\temp")))).Returns(true); - mockDirectoryHelper.Setup(x => x.Delete(It.Is(y => y.Contains(@"E:\temp")), true)).Throws(new FileNotFoundException()); - using (var attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger, _eqtTrace, @"E:\temp", _mockFileHelper.Object, mockDirectoryHelper.Object, _mockCountDownEvent.Object)) - { - _mockDataCollectionSink.Raise(x => x.SendFileCompleted += null, new AsyncCompletedEventArgs(null, false, null)); - } - _mockDataCollectionLogger.Verify(x => x.LogWarning(_dataCollectionContext, - It.Is(y => y.Contains("CoverletDataCollectorException: CoverletCoverageDataCollector: Failed to cleanup report directory"))), Times.AtLeastOnce); - } + var mockDirectoryHelper = new Mock(); + mockDirectoryHelper.Setup(x => x.Exists(It.Is(y => y.Contains(@"E:\temp")))).Returns(true); + mockDirectoryHelper.Setup(x => x.Delete(It.Is(y => y.Contains(@"E:\temp")), true)).Throws(new FileNotFoundException()); + using (var attachmentManager = new AttachmentManager(_mockDataCollectionSink.Object, _dataCollectionContext, _testPlatformLogger, _eqtTrace, @"E:\temp", _mockFileHelper.Object, mockDirectoryHelper.Object, _mockCountDownEvent.Object)) + { + _mockDataCollectionSink.Raise(x => x.SendFileCompleted += null, new AsyncCompletedEventArgs(null, false, null)); + } + _mockDataCollectionLogger.Verify(x => x.LogWarning(_dataCollectionContext, + It.Is(y => y.Contains("CoverletDataCollectorException: CoverletCoverageDataCollector: Failed to cleanup report directory"))), Times.AtLeastOnce); } + } } diff --git a/test/coverlet.collector.tests/CoverletCoverageDataCollectorTests.cs b/test/coverlet.collector.tests/CoverletCoverageDataCollectorTests.cs index 6bb1af73b..1bf60d8a0 100644 --- a/test/coverlet.collector.tests/CoverletCoverageDataCollectorTests.cs +++ b/test/coverlet.collector.tests/CoverletCoverageDataCollectorTests.cs @@ -6,277 +6,277 @@ using System.IO; using System.Linq; using System.Xml; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; -using Moq; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Coverlet.Core; -using Coverlet.Collector.Utilities.Interfaces; -using Coverlet.Collector.Utilities; -using Xunit; using Coverlet.Collector.DataCollection; -using Coverlet.Core.Reporters; +using Coverlet.Collector.Utilities; +using Coverlet.Collector.Utilities.Interfaces; +using Coverlet.Core; using Coverlet.Core.Abstractions; using Coverlet.Core.Helpers; +using Coverlet.Core.Reporters; using Coverlet.Core.Symbols; using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; +using Moq; +using Xunit; namespace Coverlet.Collector.Tests { - public class CoverletCoverageDataCollectorTests + public class CoverletCoverageDataCollectorTests + { + private readonly DataCollectionEnvironmentContext _context; + private CoverletCoverageCollector _coverletCoverageDataCollector; + private readonly DataCollectionContext _dataCollectionContext; + private readonly Mock _mockDataCollectionEvents; + private readonly Mock _mockDataCollectionSink; + private readonly Mock _mockCoverageWrapper; + private readonly Mock _mockCountDownEventFactory; + private XmlElement _configurationElement; + private readonly Mock _mockLogger; + private readonly Mock _mockAssemblyAdapter; + + public CoverletCoverageDataCollectorTests() + { + _mockDataCollectionEvents = new Mock(); + _mockDataCollectionSink = new Mock(); + _mockLogger = new Mock(); + _configurationElement = null; + + var testcase = new TestCase { Id = Guid.NewGuid() }; + _dataCollectionContext = new DataCollectionContext(testcase); + _context = new DataCollectionEnvironmentContext(_dataCollectionContext); + _mockCoverageWrapper = new Mock(); + _mockCountDownEventFactory = new Mock(); + _mockCountDownEventFactory.Setup(def => def.Create(It.IsAny(), It.IsAny())).Returns(new Mock().Object); + _mockAssemblyAdapter = new Mock(); + _mockAssemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("abc"); + } + + [Fact] + public void OnSessionStartShouldInitializeCoverageWithCorrectCoverletSettings() + { + Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => + { + IServiceCollection serviceCollection = new ServiceCollection(); + var fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string testLib) => testLib == "abc.dll"); + serviceCollection.AddTransient(_ => fileSystem.Object); + + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), _mockAssemblyAdapter.Object)); + serviceCollection.AddSingleton(); + return serviceCollection; + }; + _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object, serviceCollectionFactory); + _coverletCoverageDataCollector.Initialize( + _configurationElement, + _mockDataCollectionEvents.Object, + _mockDataCollectionSink.Object, + _mockLogger.Object, + _context); + IDictionary sessionStartProperties = new Dictionary(); + + sessionStartProperties.Add("TestSources", new List { "abc.dll" }); + + _mockDataCollectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); + + _mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is(y => string.Equals(y.TestModule, "abc.dll")), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public void OnSessionStartShouldPrepareModulesForCoverage() + { + Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => + { + IServiceCollection serviceCollection = new ServiceCollection(); + var fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string testLib) => testLib == "abc.dll"); + serviceCollection.AddTransient(_ => fileSystem.Object); + + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), _mockAssemblyAdapter.Object)); + serviceCollection.AddSingleton(); + return serviceCollection; + }; + _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object, serviceCollectionFactory); + _coverletCoverageDataCollector.Initialize( + _configurationElement, + _mockDataCollectionEvents.Object, + _mockDataCollectionSink.Object, + null, + _context); + IDictionary sessionStartProperties = new Dictionary(); + IInstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object); + + var parameters = new CoverageParameters + { + IncludeFilters = null, + IncludeDirectories = null, + ExcludedSourceFiles = null, + ExcludeAttributes = null, + IncludeTestAssembly = true, + SingleHit = true, + MergeWith = "abc.json", + UseSourceLink = true + }; + + var coverage = new Coverage("abc.dll", parameters, It.IsAny(), instrumentationHelper, new Mock().Object, new Mock().Object, new Mock().Object); + + sessionStartProperties.Add("TestSources", new List { "abc.dll" }); + _mockCoverageWrapper.Setup(x => x.CreateCoverage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(coverage); + + _mockDataCollectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); + + _mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is(y => y.TestModule.Contains("abc.dll")), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + _mockCoverageWrapper.Verify(x => x.PrepareModules(It.IsAny()), Times.Once); + } + + [Fact] + public void OnSessionEndShouldSendGetCoverageReportToTestPlatform() + { + Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => + { + IServiceCollection serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), _mockAssemblyAdapter.Object)); + serviceCollection.AddSingleton(); + return serviceCollection; + }; + _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), new CoverageWrapper(), _mockCountDownEventFactory.Object, serviceCollectionFactory); + _coverletCoverageDataCollector.Initialize( + _configurationElement, + _mockDataCollectionEvents.Object, + _mockDataCollectionSink.Object, + _mockLogger.Object, + _context); + + string module = GetType().Assembly.Location; + string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb"); + + DirectoryInfo 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); + + IDictionary sessionStartProperties = new Dictionary(); + sessionStartProperties.Add("TestSources", new List { Path.Combine(directory.FullName, Path.GetFileName(module)) }); + + _mockDataCollectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); + _mockDataCollectionEvents.Raise(x => x.SessionEnd += null, new SessionEndEventArgs()); + + _mockDataCollectionSink.Verify(x => x.SendFileAsync(It.IsAny()), Times.Once); + + directory.Delete(true); + } + + [Theory] + [InlineData("noValidFormat", 0)] + [InlineData("json,cobertura", 2)] + [InlineData("json,cobertura,lcov", 3)] + public void OnSessionEndShouldSendCoverageReportsForMultipleFormatsToTestPlatform(string formats, int sendReportsCount) + { + Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => + { + IServiceCollection serviceCollection = new ServiceCollection(); + var fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string testLib) => testLib == "Test"); + serviceCollection.AddTransient(_ => fileSystem.Object); + + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), _mockAssemblyAdapter.Object)); + serviceCollection.AddSingleton(); + return serviceCollection; + }; + _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), new CoverageWrapper(), _mockCountDownEventFactory.Object, serviceCollectionFactory); + + IList reporters = formats.Split(',').Select(f => new ReporterFactory(f).CreateReporter()).Where(x => x != null).ToList(); + var mockDataCollectionSink = new Mock(); + mockDataCollectionSink.Setup(m => m.SendFileAsync(It.IsAny())).Callback(fti => + { + reporters.Remove(reporters.First(x => + Path.GetFileName(fti.Path) == Path.ChangeExtension(CoverletConstants.DefaultFileName, x.Extension)) + ); + }); + + var doc = new XmlDocument(); + XmlElement root = doc.CreateElement("Configuration"); + XmlElement element = doc.CreateElement("Format"); + element.AppendChild(doc.CreateTextNode(formats)); + root.AppendChild(element); + + _configurationElement = root; + + _coverletCoverageDataCollector.Initialize( + _configurationElement, + _mockDataCollectionEvents.Object, + mockDataCollectionSink.Object, + _mockLogger.Object, + _context); + + var sessionStartProperties = new Dictionary { { "TestSources", new List { "Test" } } }; + + _mockDataCollectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); + _mockDataCollectionEvents.Raise(x => x.SessionEnd += null, new SessionEndEventArgs()); + + mockDataCollectionSink.Verify(x => x.SendFileAsync(It.IsAny()), Times.Exactly(sendReportsCount)); + Assert.Empty(reporters); + } + + [Fact] + public void OnSessionStartShouldLogWarningIfInstrumentationFailed() { - private readonly DataCollectionEnvironmentContext _context; - private CoverletCoverageCollector _coverletCoverageDataCollector; - private readonly DataCollectionContext _dataCollectionContext; - private readonly Mock _mockDataCollectionEvents; - private readonly Mock _mockDataCollectionSink; - private readonly Mock _mockCoverageWrapper; - private readonly Mock _mockCountDownEventFactory; - private XmlElement _configurationElement; - private readonly Mock _mockLogger; - private readonly Mock _mockAssemblyAdapter; - - public CoverletCoverageDataCollectorTests() - { - _mockDataCollectionEvents = new Mock(); - _mockDataCollectionSink = new Mock(); - _mockLogger = new Mock(); - _configurationElement = null; - - var testcase = new TestCase { Id = Guid.NewGuid() }; - _dataCollectionContext = new DataCollectionContext(testcase); - _context = new DataCollectionEnvironmentContext(_dataCollectionContext); - _mockCoverageWrapper = new Mock(); - _mockCountDownEventFactory = new Mock(); - _mockCountDownEventFactory.Setup(def => def.Create(It.IsAny(), It.IsAny())).Returns(new Mock().Object); - _mockAssemblyAdapter = new Mock(); - _mockAssemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("abc"); - } - - [Fact] - public void OnSessionStartShouldInitializeCoverageWithCorrectCoverletSettings() - { - Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => - { - IServiceCollection serviceCollection = new ServiceCollection(); - var fileSystem = new Mock(); - fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string testLib) => testLib == "abc.dll"); - serviceCollection.AddTransient(_ => fileSystem.Object); - - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), _mockAssemblyAdapter.Object)); - serviceCollection.AddSingleton(); - return serviceCollection; - }; - _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object, serviceCollectionFactory); - _coverletCoverageDataCollector.Initialize( - _configurationElement, - _mockDataCollectionEvents.Object, - _mockDataCollectionSink.Object, - _mockLogger.Object, - _context); - IDictionary sessionStartProperties = new Dictionary(); - - sessionStartProperties.Add("TestSources", new List { "abc.dll" }); - - _mockDataCollectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); - - _mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is(y => string.Equals(y.TestModule, "abc.dll")), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public void OnSessionStartShouldPrepareModulesForCoverage() - { - Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => - { - IServiceCollection serviceCollection = new ServiceCollection(); - var fileSystem = new Mock(); - fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string testLib) => testLib == "abc.dll"); - serviceCollection.AddTransient(_ => fileSystem.Object); - - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), _mockAssemblyAdapter.Object)); - serviceCollection.AddSingleton(); - return serviceCollection; - }; - _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object, serviceCollectionFactory); - _coverletCoverageDataCollector.Initialize( - _configurationElement, - _mockDataCollectionEvents.Object, - _mockDataCollectionSink.Object, - null, - _context); - IDictionary sessionStartProperties = new Dictionary(); - IInstrumentationHelper instrumentationHelper = - new InstrumentationHelper(new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - new Mock().Object); - - var parameters = new CoverageParameters - { - IncludeFilters = null, - IncludeDirectories = null, - ExcludedSourceFiles = null, - ExcludeAttributes = null, - IncludeTestAssembly = true, - SingleHit = true, - MergeWith = "abc.json", - UseSourceLink = true - }; - - var coverage = new Coverage("abc.dll", parameters, It.IsAny(), instrumentationHelper, new Mock().Object, new Mock().Object, new Mock().Object); - - sessionStartProperties.Add("TestSources", new List { "abc.dll" }); - _mockCoverageWrapper.Setup(x => x.CreateCoverage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(coverage); - - _mockDataCollectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); - - _mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is(y => y.TestModule.Contains("abc.dll")), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - _mockCoverageWrapper.Verify(x => x.PrepareModules(It.IsAny()), Times.Once); - } - - [Fact] - public void OnSessionEndShouldSendGetCoverageReportToTestPlatform() - { - Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => - { - IServiceCollection serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), _mockAssemblyAdapter.Object)); - serviceCollection.AddSingleton(); - return serviceCollection; - }; - _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), new CoverageWrapper(), _mockCountDownEventFactory.Object, serviceCollectionFactory); - _coverletCoverageDataCollector.Initialize( - _configurationElement, - _mockDataCollectionEvents.Object, - _mockDataCollectionSink.Object, - _mockLogger.Object, - _context); - - string module = GetType().Assembly.Location; - string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb"); - - DirectoryInfo 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); - - IDictionary sessionStartProperties = new Dictionary(); - sessionStartProperties.Add("TestSources", new List { Path.Combine(directory.FullName, Path.GetFileName(module)) }); - - _mockDataCollectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); - _mockDataCollectionEvents.Raise(x => x.SessionEnd += null, new SessionEndEventArgs()); - - _mockDataCollectionSink.Verify(x => x.SendFileAsync(It.IsAny()), Times.Once); - - directory.Delete(true); - } - - [Theory] - [InlineData("noValidFormat", 0)] - [InlineData("json,cobertura", 2)] - [InlineData("json,cobertura,lcov", 3)] - public void OnSessionEndShouldSendCoverageReportsForMultipleFormatsToTestPlatform(string formats, int sendReportsCount) - { - Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => - { - IServiceCollection serviceCollection = new ServiceCollection(); - var fileSystem = new Mock(); - fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string testLib) => testLib == "Test"); - serviceCollection.AddTransient(_ => fileSystem.Object); - - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), _mockAssemblyAdapter.Object)); - serviceCollection.AddSingleton(); - return serviceCollection; - }; - _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), new CoverageWrapper(), _mockCountDownEventFactory.Object, serviceCollectionFactory); - - IList reporters = formats.Split(',').Select(f => new ReporterFactory(f).CreateReporter()).Where(x => x != null).ToList(); - var mockDataCollectionSink = new Mock(); - mockDataCollectionSink.Setup(m => m.SendFileAsync(It.IsAny())).Callback(fti => - { - reporters.Remove(reporters.First(x => - Path.GetFileName(fti.Path) == Path.ChangeExtension(CoverletConstants.DefaultFileName, x.Extension)) - ); - }); - - var doc = new XmlDocument(); - XmlElement root = doc.CreateElement("Configuration"); - XmlElement element = doc.CreateElement("Format"); - element.AppendChild(doc.CreateTextNode(formats)); - root.AppendChild(element); - - _configurationElement = root; - - _coverletCoverageDataCollector.Initialize( - _configurationElement, - _mockDataCollectionEvents.Object, - mockDataCollectionSink.Object, - _mockLogger.Object, - _context); - - var sessionStartProperties = new Dictionary { { "TestSources", new List { "Test" } } }; - - _mockDataCollectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); - _mockDataCollectionEvents.Raise(x => x.SessionEnd += null, new SessionEndEventArgs()); - - mockDataCollectionSink.Verify(x => x.SendFileAsync(It.IsAny()), Times.Exactly(sendReportsCount)); - Assert.Empty(reporters); - } - - [Fact] - public void OnSessionStartShouldLogWarningIfInstrumentationFailed() - { - Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => - { - IServiceCollection serviceCollection = new ServiceCollection(); - var fileSystem = new Mock(); - fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string testLib) => testLib == "abc.dll"); - serviceCollection.AddTransient(_ => fileSystem.Object); - - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), _mockAssemblyAdapter.Object)); - serviceCollection.AddSingleton(); - return serviceCollection; - }; - _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object, serviceCollectionFactory); - _coverletCoverageDataCollector.Initialize( - _configurationElement, - _mockDataCollectionEvents.Object, - _mockDataCollectionSink.Object, - _mockLogger.Object, - _context); - IDictionary sessionStartProperties = new Dictionary(); - - sessionStartProperties.Add("TestSources", new List { "abc.dll" }); - - _mockCoverageWrapper.Setup(x => x.PrepareModules(It.IsAny())).Throws(new FileNotFoundException()); - - _mockDataCollectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); - - _mockLogger.Verify(x => x.LogWarning(_dataCollectionContext, - It.Is(y => y.Contains("CoverletDataCollectorException")))); - } + Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => + { + IServiceCollection serviceCollection = new ServiceCollection(); + var fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string testLib) => testLib == "abc.dll"); + serviceCollection.AddTransient(_ => fileSystem.Object); + + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), _mockAssemblyAdapter.Object)); + serviceCollection.AddSingleton(); + return serviceCollection; + }; + _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object, serviceCollectionFactory); + _coverletCoverageDataCollector.Initialize( + _configurationElement, + _mockDataCollectionEvents.Object, + _mockDataCollectionSink.Object, + _mockLogger.Object, + _context); + IDictionary sessionStartProperties = new Dictionary(); + + sessionStartProperties.Add("TestSources", new List { "abc.dll" }); + + _mockCoverageWrapper.Setup(x => x.PrepareModules(It.IsAny())).Throws(new FileNotFoundException()); + + _mockDataCollectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); + + _mockLogger.Verify(x => x.LogWarning(_dataCollectionContext, + It.Is(y => y.Contains("CoverletDataCollectorException")))); } + } } diff --git a/test/coverlet.collector.tests/CoverletSettingsParserTests.cs b/test/coverlet.collector.tests/CoverletSettingsParserTests.cs index 191bdb232..9578a5407 100644 --- a/test/coverlet.collector.tests/CoverletSettingsParserTests.cs +++ b/test/coverlet.collector.tests/CoverletSettingsParserTests.cs @@ -10,196 +10,196 @@ namespace Coverlet.Collector.Tests { - public class CoverletSettingsParserTests + public class CoverletSettingsParserTests + { + private readonly CoverletSettingsParser _coverletSettingsParser; + + public CoverletSettingsParserTests() + { + _coverletSettingsParser = new CoverletSettingsParser(new TestPlatformEqtTrace()); + } + + [Fact] + public void ParseShouldThrowCoverletDataCollectorExceptionIfTestModulesIsNull() + { + string message = Assert.Throws(() => _coverletSettingsParser.Parse(null, null)).Message; + + Assert.Equal("CoverletCoverageDataCollector: No test modules found", message); + } + + [Fact] + public void ParseShouldThrowCoverletDataCollectorExceptionIfTestModulesIsEmpty() + { + string message = Assert.Throws(() => _coverletSettingsParser.Parse(null, Enumerable.Empty())).Message; + + Assert.Equal("CoverletCoverageDataCollector: No test modules found", message); + } + + [Fact] + public void ParseShouldSelectFirstTestModuleFromTestModulesList() + { + var testModules = new List { "module1.dll", "module2.dll", "module3.dll" }; + + CoverletSettings coverletSettings = _coverletSettingsParser.Parse(null, testModules); + + Assert.Equal("module1.dll", coverletSettings.TestModule); + } + + [Theory] + [InlineData("[*]*,[coverlet]*", "[coverlet.*.tests?]*,[coverlet.*.tests.*]*", @"E:\temp,/var/tmp", "module1.cs,module2.cs", "Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute", "DoesNotReturnAttribute,ThrowsAttribute")] + [InlineData("[*]*,,[coverlet]*", "[coverlet.*.tests?]*,,[coverlet.*.tests.*]*", @"E:\temp,,/var/tmp", "module1.cs,,module2.cs", "Obsolete,,GeneratedCodeAttribute,,CompilerGeneratedAttribute", "DoesNotReturnAttribute,,ThrowsAttribute")] + [InlineData("[*]*, ,[coverlet]*", "[coverlet.*.tests?]*, ,[coverlet.*.tests.*]*", @"E:\temp, ,/var/tmp", "module1.cs, ,module2.cs", "Obsolete, ,GeneratedCodeAttribute, ,CompilerGeneratedAttribute", "DoesNotReturnAttribute, ,ThrowsAttribute")] + [InlineData("[*]*,\t,[coverlet]*", "[coverlet.*.tests?]*,\t,[coverlet.*.tests.*]*", "E:\\temp,\t,/var/tmp", "module1.cs,\t,module2.cs", "Obsolete,\t,GeneratedCodeAttribute,\t,CompilerGeneratedAttribute", "DoesNotReturnAttribute,\t,ThrowsAttribute")] + [InlineData("[*]*, [coverlet]*", "[coverlet.*.tests?]*, [coverlet.*.tests.*]*", @"E:\temp, /var/tmp", "module1.cs, module2.cs", "Obsolete, GeneratedCodeAttribute, CompilerGeneratedAttribute", "DoesNotReturnAttribute, ThrowsAttribute")] + [InlineData("[*]*,\t[coverlet]*", "[coverlet.*.tests?]*,\t[coverlet.*.tests.*]*", "E:\\temp,\t/var/tmp", "module1.cs,\tmodule2.cs", "Obsolete,\tGeneratedCodeAttribute,\tCompilerGeneratedAttribute", "DoesNotReturnAttribute,\tThrowsAttribute")] + [InlineData("[*]*, \t[coverlet]*", "[coverlet.*.tests?]*, \t[coverlet.*.tests.*]*", "E:\\temp, \t/var/tmp", "module1.cs, \tmodule2.cs", "Obsolete, \tGeneratedCodeAttribute, \tCompilerGeneratedAttribute", "DoesNotReturnAttribute, \tThrowsAttribute")] + [InlineData("[*]*,\r\n[coverlet]*", "[coverlet.*.tests?]*,\r\n[coverlet.*.tests.*]*", "E:\\temp,\r\n/var/tmp", "module1.cs,\r\nmodule2.cs", "Obsolete,\r\nGeneratedCodeAttribute,\r\nCompilerGeneratedAttribute", "DoesNotReturnAttribute,\r\nThrowsAttribute")] + [InlineData("[*]*, \r\n [coverlet]*", "[coverlet.*.tests?]*, \r\n [coverlet.*.tests.*]*", "E:\\temp, \r\n /var/tmp", "module1.cs, \r\n module2.cs", "Obsolete, \r\n GeneratedCodeAttribute, \r\n CompilerGeneratedAttribute", "DoesNotReturnAttribute, \r\n ThrowsAttribute")] + [InlineData("[*]*,\t\r\n\t[coverlet]*", "[coverlet.*.tests?]*,\t\r\n\t[coverlet.*.tests.*]*", "E:\\temp,\t\r\n\t/var/tmp", "module1.cs,\t\r\n\tmodule2.cs", "Obsolete,\t\r\n\tGeneratedCodeAttribute,\t\r\n\tCompilerGeneratedAttribute", "DoesNotReturnAttribute,\t\r\n\tThrowsAttribute")] + [InlineData("[*]*, \t \r\n \t [coverlet]*", "[coverlet.*.tests?]*, \t \r\n \t [coverlet.*.tests.*]*", "E:\\temp, \t \r\n \t /var/tmp", "module1.cs, \t \r\n \t module2.cs", "Obsolete, \t \r\n \t GeneratedCodeAttribute, \t \r\n \t CompilerGeneratedAttribute", "DoesNotReturnAttribute, \t \r\n \t ThrowsAttribute")] + [InlineData(" [*]* , [coverlet]* ", " [coverlet.*.tests?]* , [coverlet.*.tests.*]* ", " E:\\temp , /var/tmp ", " module1.cs , module2.cs ", " Obsolete , GeneratedCodeAttribute , CompilerGeneratedAttribute ", "DoesNotReturnAttribute , ThrowsAttribute")] + public void ParseShouldCorrectlyParseConfigurationElement(string includeFilters, + string excludeFilters, + string includeDirectories, + string excludeSourceFiles, + string excludeAttributes, + string doesNotReturnAttributes) + { + var testModules = new List { "abc.dll" }; + var doc = new XmlDocument(); + XmlElement configElement = doc.CreateElement("Configuration"); + CreateCoverletNodes(doc, configElement, CoverletConstants.IncludeFiltersElementName, includeFilters); + CreateCoverletNodes(doc, configElement, CoverletConstants.ExcludeFiltersElementName, excludeFilters); + CreateCoverletNodes(doc, configElement, CoverletConstants.IncludeDirectoriesElementName, includeDirectories); + CreateCoverletNodes(doc, configElement, CoverletConstants.ExcludeSourceFilesElementName, excludeSourceFiles); + CreateCoverletNodes(doc, configElement, CoverletConstants.ExcludeAttributesElementName, excludeAttributes); + CreateCoverletNodes(doc, configElement, CoverletConstants.MergeWithElementName, "/path/to/result.json"); + CreateCoverletNodes(doc, configElement, CoverletConstants.UseSourceLinkElementName, "false"); + CreateCoverletNodes(doc, configElement, CoverletConstants.SingleHitElementName, "true"); + CreateCoverletNodes(doc, configElement, CoverletConstants.IncludeTestAssemblyElementName, "true"); + CreateCoverletNodes(doc, configElement, CoverletConstants.SkipAutoProps, "true"); + CreateCoverletNodes(doc, configElement, CoverletConstants.DeterministicReport, "true"); + CreateCoverletNodes(doc, configElement, CoverletConstants.DoesNotReturnAttributesElementName, doesNotReturnAttributes); + + CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules); + + Assert.Equal("abc.dll", coverletSettings.TestModule); + Assert.Equal("[*]*", coverletSettings.IncludeFilters[0]); + Assert.Equal("[coverlet]*", coverletSettings.IncludeFilters[1]); + Assert.Equal(@"E:\temp", coverletSettings.IncludeDirectories[0]); + Assert.Equal("/var/tmp", coverletSettings.IncludeDirectories[1]); + Assert.Equal("module1.cs", coverletSettings.ExcludeSourceFiles[0]); + Assert.Equal("module2.cs", coverletSettings.ExcludeSourceFiles[1]); + Assert.Equal("Obsolete", coverletSettings.ExcludeAttributes[0]); + Assert.Equal("GeneratedCodeAttribute", coverletSettings.ExcludeAttributes[1]); + Assert.Equal("CompilerGeneratedAttribute", coverletSettings.ExcludeAttributes[2]); + Assert.Equal("/path/to/result.json", coverletSettings.MergeWith); + Assert.Equal("[coverlet.*]*", coverletSettings.ExcludeFilters[0]); + Assert.Equal("[coverlet.*.tests?]*", coverletSettings.ExcludeFilters[1]); + Assert.Equal("[coverlet.*.tests.*]*", coverletSettings.ExcludeFilters[2]); + Assert.Equal("DoesNotReturnAttribute", coverletSettings.DoesNotReturnAttributes[0]); + Assert.Equal("ThrowsAttribute", coverletSettings.DoesNotReturnAttributes[1]); + + Assert.False(coverletSettings.UseSourceLink); + Assert.True(coverletSettings.SingleHit); + Assert.True(coverletSettings.IncludeTestAssembly); + Assert.True(coverletSettings.SkipAutoProps); + Assert.True(coverletSettings.DeterministicReport); + } + + [Fact] + public void ParseShouldCorrectlyParseConfigurationElementWithNullInnerText() + { + var testModules = new List { "abc.dll" }; + var doc = new XmlDocument(); + XmlElement configElement = doc.CreateElement("Configuration"); + CreateCoverletNullInnerTextNodes(doc, configElement, CoverletConstants.IncludeFiltersElementName); + CreateCoverletNullInnerTextNodes(doc, configElement, CoverletConstants.ExcludeFiltersElementName); + CreateCoverletNullInnerTextNodes(doc, configElement, CoverletConstants.IncludeDirectoriesElementName); + CreateCoverletNullInnerTextNodes(doc, configElement, CoverletConstants.ExcludeSourceFilesElementName); + CreateCoverletNullInnerTextNodes(doc, configElement, CoverletConstants.ExcludeAttributesElementName); + + CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules); + + Assert.Equal("abc.dll", coverletSettings.TestModule); + Assert.Empty(coverletSettings.IncludeFilters); + Assert.Empty(coverletSettings.IncludeDirectories); + Assert.Empty(coverletSettings.ExcludeSourceFiles); + Assert.Empty(coverletSettings.ExcludeAttributes); + Assert.Single(coverletSettings.ExcludeFilters, "[coverlet.*]*"); + } + + [Fact] + public void ParseShouldCorrectlyParseConfigurationElementWithNullElements() + { + var testModules = new List { "abc.dll" }; + var doc = new XmlDocument(); + XmlElement configElement = doc.CreateElement("Configuration"); + + CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules); + + Assert.Equal("abc.dll", coverletSettings.TestModule); + Assert.Null(coverletSettings.IncludeFilters); + Assert.Null(coverletSettings.IncludeDirectories); + Assert.Null(coverletSettings.ExcludeSourceFiles); + Assert.Null(coverletSettings.ExcludeAttributes); + Assert.Single(coverletSettings.ExcludeFilters, "[coverlet.*]*"); + } + + [Theory] + [InlineData(" , json", 1, new[] { "json" })] + [InlineData(" , json, ", 1, new[] { "json" })] + [InlineData("json,cobertura", 2, new[] { "json", "cobertura" })] + [InlineData("json,\r\ncobertura", 2, new[] { "json", "cobertura" })] + [InlineData("json, \r\n cobertura", 2, new[] { "json", "cobertura" })] + [InlineData("json,\tcobertura", 2, new[] { "json", "cobertura" })] + [InlineData("json, \t cobertura", 2, new[] { "json", "cobertura" })] + [InlineData("json,\t\r\n\tcobertura", 2, new[] { "json", "cobertura" })] + [InlineData("json, \t \r\n \tcobertura", 2, new[] { "json", "cobertura" })] + [InlineData(" , json,, cobertura ", 2, new[] { "json", "cobertura" })] + [InlineData(" , json, , cobertura ", 2, new[] { "json", "cobertura" })] + [InlineData(",json,\t,\r\n,cobertura", 2, new[] { "json", "cobertura" })] + public void ParseShouldCorrectlyParseMultipleFormats(string formats, int formatsCount, string[] expectedReportFormats) + { + var testModules = new List { "abc.dll" }; + var doc = new XmlDocument(); + XmlElement configElement = doc.CreateElement("Configuration"); + CreateCoverletNodes(doc, configElement, CoverletConstants.ReportFormatElementName, formats); + + CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules); + + Assert.Equal(expectedReportFormats, coverletSettings.ReportFormats); + Assert.Equal(formatsCount, coverletSettings.ReportFormats.Length); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void ParseShouldUseDefaultFormatWhenNoFormatSpecified(string formats) + { + var testModules = new List { "abc.dll" }; + string defaultFormat = CoverletConstants.DefaultReportFormat; + var doc = new XmlDocument(); + XmlElement configElement = doc.CreateElement("Configuration"); + CreateCoverletNodes(doc, configElement, CoverletConstants.ReportFormatElementName, formats); + + CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules); + + Assert.Equal(defaultFormat, coverletSettings.ReportFormats[0]); + } + + private static void CreateCoverletNodes(XmlDocument doc, XmlElement configElement, string nodeSetting, string nodeValue) + { + XmlNode node = doc.CreateNode("element", nodeSetting, string.Empty); + node.InnerText = nodeValue; + configElement.AppendChild(node); + } + + private static void CreateCoverletNullInnerTextNodes(XmlDocument doc, XmlElement configElement, string nodeSetting) { - private readonly CoverletSettingsParser _coverletSettingsParser; - - public CoverletSettingsParserTests() - { - _coverletSettingsParser = new CoverletSettingsParser(new TestPlatformEqtTrace()); - } - - [Fact] - public void ParseShouldThrowCoverletDataCollectorExceptionIfTestModulesIsNull() - { - string message = Assert.Throws(() => _coverletSettingsParser.Parse(null, null)).Message; - - Assert.Equal("CoverletCoverageDataCollector: No test modules found", message); - } - - [Fact] - public void ParseShouldThrowCoverletDataCollectorExceptionIfTestModulesIsEmpty() - { - string message = Assert.Throws(() => _coverletSettingsParser.Parse(null, Enumerable.Empty())).Message; - - Assert.Equal("CoverletCoverageDataCollector: No test modules found", message); - } - - [Fact] - public void ParseShouldSelectFirstTestModuleFromTestModulesList() - { - var testModules = new List { "module1.dll", "module2.dll", "module3.dll" }; - - CoverletSettings coverletSettings = _coverletSettingsParser.Parse(null, testModules); - - Assert.Equal("module1.dll", coverletSettings.TestModule); - } - - [Theory] - [InlineData("[*]*,[coverlet]*", "[coverlet.*.tests?]*,[coverlet.*.tests.*]*", @"E:\temp,/var/tmp", "module1.cs,module2.cs", "Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute", "DoesNotReturnAttribute,ThrowsAttribute")] - [InlineData("[*]*,,[coverlet]*", "[coverlet.*.tests?]*,,[coverlet.*.tests.*]*", @"E:\temp,,/var/tmp", "module1.cs,,module2.cs", "Obsolete,,GeneratedCodeAttribute,,CompilerGeneratedAttribute", "DoesNotReturnAttribute,,ThrowsAttribute")] - [InlineData("[*]*, ,[coverlet]*", "[coverlet.*.tests?]*, ,[coverlet.*.tests.*]*", @"E:\temp, ,/var/tmp", "module1.cs, ,module2.cs", "Obsolete, ,GeneratedCodeAttribute, ,CompilerGeneratedAttribute", "DoesNotReturnAttribute, ,ThrowsAttribute")] - [InlineData("[*]*,\t,[coverlet]*", "[coverlet.*.tests?]*,\t,[coverlet.*.tests.*]*", "E:\\temp,\t,/var/tmp", "module1.cs,\t,module2.cs", "Obsolete,\t,GeneratedCodeAttribute,\t,CompilerGeneratedAttribute", "DoesNotReturnAttribute,\t,ThrowsAttribute")] - [InlineData("[*]*, [coverlet]*", "[coverlet.*.tests?]*, [coverlet.*.tests.*]*", @"E:\temp, /var/tmp", "module1.cs, module2.cs", "Obsolete, GeneratedCodeAttribute, CompilerGeneratedAttribute", "DoesNotReturnAttribute, ThrowsAttribute")] - [InlineData("[*]*,\t[coverlet]*", "[coverlet.*.tests?]*,\t[coverlet.*.tests.*]*", "E:\\temp,\t/var/tmp", "module1.cs,\tmodule2.cs", "Obsolete,\tGeneratedCodeAttribute,\tCompilerGeneratedAttribute", "DoesNotReturnAttribute,\tThrowsAttribute")] - [InlineData("[*]*, \t[coverlet]*", "[coverlet.*.tests?]*, \t[coverlet.*.tests.*]*", "E:\\temp, \t/var/tmp", "module1.cs, \tmodule2.cs", "Obsolete, \tGeneratedCodeAttribute, \tCompilerGeneratedAttribute", "DoesNotReturnAttribute, \tThrowsAttribute")] - [InlineData("[*]*,\r\n[coverlet]*", "[coverlet.*.tests?]*,\r\n[coverlet.*.tests.*]*", "E:\\temp,\r\n/var/tmp", "module1.cs,\r\nmodule2.cs", "Obsolete,\r\nGeneratedCodeAttribute,\r\nCompilerGeneratedAttribute", "DoesNotReturnAttribute,\r\nThrowsAttribute")] - [InlineData("[*]*, \r\n [coverlet]*", "[coverlet.*.tests?]*, \r\n [coverlet.*.tests.*]*", "E:\\temp, \r\n /var/tmp", "module1.cs, \r\n module2.cs", "Obsolete, \r\n GeneratedCodeAttribute, \r\n CompilerGeneratedAttribute", "DoesNotReturnAttribute, \r\n ThrowsAttribute")] - [InlineData("[*]*,\t\r\n\t[coverlet]*", "[coverlet.*.tests?]*,\t\r\n\t[coverlet.*.tests.*]*", "E:\\temp,\t\r\n\t/var/tmp", "module1.cs,\t\r\n\tmodule2.cs", "Obsolete,\t\r\n\tGeneratedCodeAttribute,\t\r\n\tCompilerGeneratedAttribute", "DoesNotReturnAttribute,\t\r\n\tThrowsAttribute")] - [InlineData("[*]*, \t \r\n \t [coverlet]*", "[coverlet.*.tests?]*, \t \r\n \t [coverlet.*.tests.*]*", "E:\\temp, \t \r\n \t /var/tmp", "module1.cs, \t \r\n \t module2.cs", "Obsolete, \t \r\n \t GeneratedCodeAttribute, \t \r\n \t CompilerGeneratedAttribute", "DoesNotReturnAttribute, \t \r\n \t ThrowsAttribute")] - [InlineData(" [*]* , [coverlet]* ", " [coverlet.*.tests?]* , [coverlet.*.tests.*]* ", " E:\\temp , /var/tmp ", " module1.cs , module2.cs ", " Obsolete , GeneratedCodeAttribute , CompilerGeneratedAttribute ", "DoesNotReturnAttribute , ThrowsAttribute")] - public void ParseShouldCorrectlyParseConfigurationElement(string includeFilters, - string excludeFilters, - string includeDirectories, - string excludeSourceFiles, - string excludeAttributes, - string doesNotReturnAttributes) - { - var testModules = new List { "abc.dll" }; - var doc = new XmlDocument(); - XmlElement configElement = doc.CreateElement("Configuration"); - CreateCoverletNodes(doc, configElement, CoverletConstants.IncludeFiltersElementName, includeFilters); - CreateCoverletNodes(doc, configElement, CoverletConstants.ExcludeFiltersElementName, excludeFilters); - CreateCoverletNodes(doc, configElement, CoverletConstants.IncludeDirectoriesElementName, includeDirectories); - CreateCoverletNodes(doc, configElement, CoverletConstants.ExcludeSourceFilesElementName, excludeSourceFiles); - CreateCoverletNodes(doc, configElement, CoverletConstants.ExcludeAttributesElementName, excludeAttributes); - CreateCoverletNodes(doc, configElement, CoverletConstants.MergeWithElementName, "/path/to/result.json"); - CreateCoverletNodes(doc, configElement, CoverletConstants.UseSourceLinkElementName, "false"); - CreateCoverletNodes(doc, configElement, CoverletConstants.SingleHitElementName, "true"); - CreateCoverletNodes(doc, configElement, CoverletConstants.IncludeTestAssemblyElementName, "true"); - CreateCoverletNodes(doc, configElement, CoverletConstants.SkipAutoProps, "true"); - CreateCoverletNodes(doc, configElement, CoverletConstants.DeterministicReport, "true"); - CreateCoverletNodes(doc, configElement, CoverletConstants.DoesNotReturnAttributesElementName, doesNotReturnAttributes); - - CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules); - - Assert.Equal("abc.dll", coverletSettings.TestModule); - Assert.Equal("[*]*", coverletSettings.IncludeFilters[0]); - Assert.Equal("[coverlet]*", coverletSettings.IncludeFilters[1]); - Assert.Equal(@"E:\temp", coverletSettings.IncludeDirectories[0]); - Assert.Equal("/var/tmp", coverletSettings.IncludeDirectories[1]); - Assert.Equal("module1.cs", coverletSettings.ExcludeSourceFiles[0]); - Assert.Equal("module2.cs", coverletSettings.ExcludeSourceFiles[1]); - Assert.Equal("Obsolete", coverletSettings.ExcludeAttributes[0]); - Assert.Equal("GeneratedCodeAttribute", coverletSettings.ExcludeAttributes[1]); - Assert.Equal("CompilerGeneratedAttribute", coverletSettings.ExcludeAttributes[2]); - Assert.Equal("/path/to/result.json", coverletSettings.MergeWith); - Assert.Equal("[coverlet.*]*", coverletSettings.ExcludeFilters[0]); - Assert.Equal("[coverlet.*.tests?]*", coverletSettings.ExcludeFilters[1]); - Assert.Equal("[coverlet.*.tests.*]*", coverletSettings.ExcludeFilters[2]); - Assert.Equal("DoesNotReturnAttribute", coverletSettings.DoesNotReturnAttributes[0]); - Assert.Equal("ThrowsAttribute", coverletSettings.DoesNotReturnAttributes[1]); - - Assert.False(coverletSettings.UseSourceLink); - Assert.True(coverletSettings.SingleHit); - Assert.True(coverletSettings.IncludeTestAssembly); - Assert.True(coverletSettings.SkipAutoProps); - Assert.True(coverletSettings.DeterministicReport); - } - - [Fact] - public void ParseShouldCorrectlyParseConfigurationElementWithNullInnerText() - { - var testModules = new List { "abc.dll" }; - var doc = new XmlDocument(); - XmlElement configElement = doc.CreateElement("Configuration"); - CreateCoverletNullInnerTextNodes(doc, configElement, CoverletConstants.IncludeFiltersElementName); - CreateCoverletNullInnerTextNodes(doc, configElement, CoverletConstants.ExcludeFiltersElementName); - CreateCoverletNullInnerTextNodes(doc, configElement, CoverletConstants.IncludeDirectoriesElementName); - CreateCoverletNullInnerTextNodes(doc, configElement, CoverletConstants.ExcludeSourceFilesElementName); - CreateCoverletNullInnerTextNodes(doc, configElement, CoverletConstants.ExcludeAttributesElementName); - - CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules); - - Assert.Equal("abc.dll", coverletSettings.TestModule); - Assert.Empty(coverletSettings.IncludeFilters); - Assert.Empty(coverletSettings.IncludeDirectories); - Assert.Empty(coverletSettings.ExcludeSourceFiles); - Assert.Empty(coverletSettings.ExcludeAttributes); - Assert.Single(coverletSettings.ExcludeFilters, "[coverlet.*]*"); - } - - [Fact] - public void ParseShouldCorrectlyParseConfigurationElementWithNullElements() - { - var testModules = new List { "abc.dll" }; - var doc = new XmlDocument(); - XmlElement configElement = doc.CreateElement("Configuration"); - - CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules); - - Assert.Equal("abc.dll", coverletSettings.TestModule); - Assert.Null(coverletSettings.IncludeFilters); - Assert.Null(coverletSettings.IncludeDirectories); - Assert.Null(coverletSettings.ExcludeSourceFiles); - Assert.Null(coverletSettings.ExcludeAttributes); - Assert.Single(coverletSettings.ExcludeFilters, "[coverlet.*]*"); - } - - [Theory] - [InlineData(" , json", 1, new[] { "json" })] - [InlineData(" , json, ", 1, new[] { "json" })] - [InlineData("json,cobertura", 2, new[] { "json", "cobertura" })] - [InlineData("json,\r\ncobertura", 2, new[] { "json", "cobertura" })] - [InlineData("json, \r\n cobertura", 2, new[] { "json", "cobertura" })] - [InlineData("json,\tcobertura", 2, new[] { "json", "cobertura" })] - [InlineData("json, \t cobertura", 2, new[] { "json", "cobertura" })] - [InlineData("json,\t\r\n\tcobertura", 2, new[] { "json", "cobertura" })] - [InlineData("json, \t \r\n \tcobertura", 2, new[] { "json", "cobertura" })] - [InlineData(" , json,, cobertura ", 2, new[] { "json", "cobertura" })] - [InlineData(" , json, , cobertura ", 2, new[] { "json", "cobertura" })] - [InlineData(",json,\t,\r\n,cobertura", 2, new[] { "json", "cobertura" })] - public void ParseShouldCorrectlyParseMultipleFormats(string formats, int formatsCount, string[] expectedReportFormats) - { - var testModules = new List { "abc.dll" }; - var doc = new XmlDocument(); - XmlElement configElement = doc.CreateElement("Configuration"); - CreateCoverletNodes(doc, configElement, CoverletConstants.ReportFormatElementName, formats); - - CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules); - - Assert.Equal(expectedReportFormats, coverletSettings.ReportFormats); - Assert.Equal(formatsCount, coverletSettings.ReportFormats.Length); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - public void ParseShouldUseDefaultFormatWhenNoFormatSpecified(string formats) - { - var testModules = new List { "abc.dll" }; - string defaultFormat = CoverletConstants.DefaultReportFormat; - var doc = new XmlDocument(); - XmlElement configElement = doc.CreateElement("Configuration"); - CreateCoverletNodes(doc, configElement, CoverletConstants.ReportFormatElementName, formats); - - CoverletSettings coverletSettings = _coverletSettingsParser.Parse(configElement, testModules); - - Assert.Equal(defaultFormat, coverletSettings.ReportFormats[0]); - } - - private static void CreateCoverletNodes(XmlDocument doc, XmlElement configElement, string nodeSetting, string nodeValue) - { - XmlNode node = doc.CreateNode("element", nodeSetting, string.Empty); - node.InnerText = nodeValue; - configElement.AppendChild(node); - } - - private static void CreateCoverletNullInnerTextNodes(XmlDocument doc, XmlElement configElement, string nodeSetting) - { - XmlNode node = doc.CreateNode("element", nodeSetting, string.Empty); - node.InnerText = null; - configElement.AppendChild(node); - } + XmlNode node = doc.CreateNode("element", nodeSetting, string.Empty); + node.InnerText = null; + configElement.AppendChild(node); } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageSummaryTests.cs b/test/coverlet.core.tests/Coverage/CoverageSummaryTests.cs index beb221e9b..3fb3f0c39 100644 --- a/test/coverlet.core.tests/Coverage/CoverageSummaryTests.cs +++ b/test/coverlet.core.tests/Coverage/CoverageSummaryTests.cs @@ -6,259 +6,259 @@ namespace Coverlet.Core.Tests { - public class CoverageSummaryTests + public class CoverageSummaryTests + { + private Modules _averageCalculationSingleModule; + private Modules _averageCalculationMultiModule; + private Modules _moduleArithmeticPrecision; + + public CoverageSummaryTests() { - private Modules _averageCalculationSingleModule; - private Modules _averageCalculationMultiModule; - private Modules _moduleArithmeticPrecision; - - public CoverageSummaryTests() - { - SetupDataSingleModule(); - SetupDataMultipleModule(); - SetupDataForArithmeticPrecision(); - } - - private void SetupDataForArithmeticPrecision() - { - var lines = new Lines(); - lines.Add(1, 1); - for (int i = 2; i <= 6; i++) - { - lines.Add(i, 0); - } - var branches = new Branches(); - branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 0, Ordinal = 1 }); - for (int i = 2; i <= 6; i++) - { - branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 1, Path = 1, Ordinal = (uint)i }); - } - - var methods = new Methods(); - string methodString = "System.Void Coverlet.Core.Tests.CoverageSummaryTests::TestCalculateSummary()"; - methods.Add(methodString, new Method()); - methods[methodString].Lines = lines; - methods[methodString].Branches = branches; - - var classes = new Classes(); - classes.Add("Coverlet.Core.Tests.CoverageSummaryTests", methods); - - var documents = new Documents(); - documents.Add("doc.cs", classes); - - _moduleArithmeticPrecision = new Modules(); - _moduleArithmeticPrecision.Add("module", documents); - } - - private void SetupDataSingleModule() - { - var lines = new Lines(); - lines.Add(1, 1); - lines.Add(2, 0); - var branches = new Branches(); - branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 0, Ordinal = 1 }); - branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 1, Ordinal = 2 }); - - var methods = new Methods(); - string methodString = "System.Void Coverlet.Core.Tests.CoverageSummaryTests::TestCalculateSummary()"; - methods.Add(methodString, new Method()); - methods[methodString].Lines = lines; - methods[methodString].Branches = branches; - - var classes = new Classes(); - classes.Add("Coverlet.Core.Tests.CoverageSummaryTests", methods); - - var documents = new Documents(); - documents.Add("doc.cs", classes); - - _averageCalculationSingleModule = new Modules(); - _averageCalculationSingleModule.Add("module", documents); - } - - private void SetupDataMultipleModule() - { - var lines = new Lines + SetupDataSingleModule(); + SetupDataMultipleModule(); + SetupDataForArithmeticPrecision(); + } + + private void SetupDataForArithmeticPrecision() + { + var lines = new Lines(); + lines.Add(1, 1); + for (int i = 2; i <= 6; i++) + { + lines.Add(i, 0); + } + var branches = new Branches(); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 0, Ordinal = 1 }); + for (int i = 2; i <= 6; i++) + { + branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 1, Path = 1, Ordinal = (uint)i }); + } + + var methods = new Methods(); + string methodString = "System.Void Coverlet.Core.Tests.CoverageSummaryTests::TestCalculateSummary()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; + + var classes = new Classes(); + classes.Add("Coverlet.Core.Tests.CoverageSummaryTests", methods); + + var documents = new Documents(); + documents.Add("doc.cs", classes); + + _moduleArithmeticPrecision = new Modules(); + _moduleArithmeticPrecision.Add("module", documents); + } + + private void SetupDataSingleModule() + { + var lines = new Lines(); + lines.Add(1, 1); + lines.Add(2, 0); + var branches = new Branches(); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 0, Ordinal = 1 }); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 1, Ordinal = 2 }); + + var methods = new Methods(); + string methodString = "System.Void Coverlet.Core.Tests.CoverageSummaryTests::TestCalculateSummary()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; + + var classes = new Classes(); + classes.Add("Coverlet.Core.Tests.CoverageSummaryTests", methods); + + var documents = new Documents(); + documents.Add("doc.cs", classes); + + _averageCalculationSingleModule = new Modules(); + _averageCalculationSingleModule.Add("module", documents); + } + + private void SetupDataMultipleModule() + { + var lines = new Lines { { 1, 1 }, // covered { 2, 0 }, // not covered { 3, 0 } // not covered }; - var branches = new Branches + var branches = new Branches { new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 0, Ordinal = 1 }, // covered new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 1, Ordinal = 2 }, // covered new BranchInfo { Line = 1, Hits = 0, Offset = 1, Path = 1, Ordinal = 2 } // not covered }; - var methods = new Methods(); - string[] methodString = { + var methods = new Methods(); + string[] methodString = { "System.Void Coverlet.Core.Tests.CoverageSummaryTests::TestCalculateSummary()", // covered "System.Void Coverlet.Core.Tests.CoverageSummaryTests::TestAdditionalCalculateSummary()" // not covered }; - methods.Add(methodString[0], new Method()); - methods[methodString[0]].Lines = lines; - methods[methodString[0]].Branches = branches; + methods.Add(methodString[0], new Method()); + methods[methodString[0]].Lines = lines; + methods[methodString[0]].Branches = branches; - methods.Add(methodString[1], new Method()); - methods[methodString[1]].Lines = new Lines + methods.Add(methodString[1], new Method()); + methods[methodString[1]].Lines = new Lines { { 1, 0 } // not covered }; - var classes = new Classes + var classes = new Classes { { "Coverlet.Core.Tests.CoverageSummaryTests", methods } }; - var documents = new Documents + var documents = new Documents { { "doc.cs", classes } }; - _averageCalculationMultiModule = new Modules + _averageCalculationMultiModule = new Modules { { "module", _averageCalculationSingleModule["module"] }, { "additionalModule", documents } }; - } - - [Fact] - public void TestCalculateLineCoverage_NoModules() - { - var summary = new CoverageSummary(); - var modules = new Modules(); - - Assert.Equal(0, summary.CalculateLineCoverage(modules).Percent); - Assert.Equal(0, summary.CalculateLineCoverage(modules).AverageModulePercent); - Assert.Equal(0, summary.CalculateBranchCoverage(modules).Percent); - Assert.Equal(0, summary.CalculateBranchCoverage(modules).AverageModulePercent); - Assert.Equal(0, summary.CalculateMethodCoverage(modules).Percent); - Assert.Equal(0, summary.CalculateMethodCoverage(modules).AverageModulePercent); - } - - [Fact] - public void TestCalculateLineCoverage_SingleModule() - { - var summary = new CoverageSummary(); - - System.Collections.Generic.KeyValuePair module = _averageCalculationSingleModule.First(); - System.Collections.Generic.KeyValuePair document = module.Value.First(); - System.Collections.Generic.KeyValuePair @class = document.Value.First(); - System.Collections.Generic.KeyValuePair method = @class.Value.First(); - - Assert.Equal(50, summary.CalculateLineCoverage(_averageCalculationSingleModule).AverageModulePercent); - Assert.Equal(50, summary.CalculateLineCoverage(module.Value).Percent); - Assert.Equal(50, summary.CalculateLineCoverage(document.Value).Percent); - Assert.Equal(50, summary.CalculateLineCoverage(@class.Value).Percent); - Assert.Equal(50, summary.CalculateLineCoverage(method.Value.Lines).Percent); - } - - [Fact] - public void TestCalculateLineCoverage_MultiModule() - { - var summary = new CoverageSummary(); - Documents documentsFirstModule = _averageCalculationMultiModule["module"]; - Documents documentsSecondModule = _averageCalculationMultiModule["additionalModule"]; - - Assert.Equal(37.5, summary.CalculateLineCoverage(_averageCalculationMultiModule).AverageModulePercent); - Assert.Equal(50, summary.CalculateLineCoverage(documentsFirstModule.First().Value).Percent); - - Assert.Equal(33.33, summary.CalculateLineCoverage(documentsSecondModule.First().Value.First().Value.ElementAt(0).Value.Lines).Percent); // covered 1 of 3 - Assert.Equal(0, summary.CalculateLineCoverage(documentsSecondModule.First().Value.First().Value.ElementAt(1).Value.Lines).Percent); // covered 0 of 1 - Assert.Equal(25, summary.CalculateLineCoverage(documentsSecondModule.First().Value).Percent); // covered 1 of 4 lines - } - - [Fact] - public void TestCalculateBranchCoverage_SingleModule() - { - var summary = new CoverageSummary(); - - System.Collections.Generic.KeyValuePair module = _averageCalculationSingleModule.First(); - System.Collections.Generic.KeyValuePair document = module.Value.First(); - System.Collections.Generic.KeyValuePair @class = document.Value.First(); - System.Collections.Generic.KeyValuePair method = @class.Value.First(); - - Assert.Equal(100, summary.CalculateBranchCoverage(_averageCalculationSingleModule).AverageModulePercent); - Assert.Equal(100, summary.CalculateBranchCoverage(module.Value).Percent); - Assert.Equal(100, summary.CalculateBranchCoverage(document.Value).Percent); - Assert.Equal(100, summary.CalculateBranchCoverage(@class.Value).Percent); - Assert.Equal(100, summary.CalculateBranchCoverage(method.Value.Branches).Percent); - } - - [Fact] - public void TestCalculateBranchCoverage_MultiModule() - { - var summary = new CoverageSummary(); - Documents documentsFirstModule = _averageCalculationMultiModule["module"]; - Documents documentsSecondModule = _averageCalculationMultiModule["additionalModule"]; - - Assert.Equal(83.33, summary.CalculateBranchCoverage(_averageCalculationMultiModule).AverageModulePercent); - Assert.Equal(100, summary.CalculateBranchCoverage(documentsFirstModule.First().Value).Percent); - Assert.Equal(66.66, summary.CalculateBranchCoverage(documentsSecondModule.First().Value).Percent); - } - - [Fact] - public void TestCalculateMethodCoverage_SingleModule() - { - var summary = new CoverageSummary(); - - System.Collections.Generic.KeyValuePair module = _averageCalculationSingleModule.First(); - System.Collections.Generic.KeyValuePair document = module.Value.First(); - System.Collections.Generic.KeyValuePair @class = document.Value.First(); - System.Collections.Generic.KeyValuePair method = @class.Value.First(); - - Assert.Equal(100, summary.CalculateMethodCoverage(_averageCalculationSingleModule).AverageModulePercent); - Assert.Equal(100, summary.CalculateMethodCoverage(module.Value).Percent); - Assert.Equal(100, summary.CalculateMethodCoverage(document.Value).Percent); - Assert.Equal(100, summary.CalculateMethodCoverage(@class.Value).Percent); - Assert.Equal(100, summary.CalculateMethodCoverage(method.Value.Lines).Percent); - } - - [Fact] - public void TestCalculateMethodCoverage_MultiModule() - { - var summary = new CoverageSummary(); - Documents documentsFirstModule = _averageCalculationMultiModule["module"]; - Documents documentsSecondModule = _averageCalculationMultiModule["additionalModule"]; - - Assert.Equal(75, summary.CalculateMethodCoverage(_averageCalculationMultiModule).AverageModulePercent); - Assert.Equal(100, summary.CalculateMethodCoverage(documentsFirstModule.First().Value).Percent); - Assert.Equal(50, summary.CalculateMethodCoverage(documentsSecondModule.First().Value).Percent); - } - - [Fact] - public void TestCalculateLineCoveragePercentage_ArithmeticPrecisionCheck() - { - var summary = new CoverageSummary(); - - System.Collections.Generic.KeyValuePair module = _moduleArithmeticPrecision.First(); - System.Collections.Generic.KeyValuePair document = module.Value.First(); - System.Collections.Generic.KeyValuePair @class = document.Value.First(); - System.Collections.Generic.KeyValuePair method = @class.Value.First(); - - Assert.Equal(16.66, summary.CalculateLineCoverage(_moduleArithmeticPrecision).AverageModulePercent); - Assert.Equal(16.66, summary.CalculateLineCoverage(module.Value).Percent); - Assert.Equal(16.66, summary.CalculateLineCoverage(document.Value).Percent); - Assert.Equal(16.66, summary.CalculateLineCoverage(@class.Value).Percent); - Assert.Equal(16.66, summary.CalculateLineCoverage(method.Value.Lines).Percent); - } - - [Fact] - public void TestCalculateBranchCoveragePercentage_ArithmeticPrecisionCheck() - { - var summary = new CoverageSummary(); - - System.Collections.Generic.KeyValuePair module = _moduleArithmeticPrecision.First(); - System.Collections.Generic.KeyValuePair document = module.Value.First(); - System.Collections.Generic.KeyValuePair @class = document.Value.First(); - System.Collections.Generic.KeyValuePair method = @class.Value.First(); - - Assert.Equal(16.66, summary.CalculateBranchCoverage(_moduleArithmeticPrecision).AverageModulePercent); - Assert.Equal(16.66, summary.CalculateBranchCoverage(module.Value).Percent); - Assert.Equal(16.66, summary.CalculateBranchCoverage(document.Value).Percent); - Assert.Equal(16.66, summary.CalculateBranchCoverage(@class.Value).Percent); - Assert.Equal(16.66, summary.CalculateBranchCoverage(method.Value.Branches).Percent); - } } + + [Fact] + public void TestCalculateLineCoverage_NoModules() + { + var summary = new CoverageSummary(); + var modules = new Modules(); + + Assert.Equal(0, summary.CalculateLineCoverage(modules).Percent); + Assert.Equal(0, summary.CalculateLineCoverage(modules).AverageModulePercent); + Assert.Equal(0, summary.CalculateBranchCoverage(modules).Percent); + Assert.Equal(0, summary.CalculateBranchCoverage(modules).AverageModulePercent); + Assert.Equal(0, summary.CalculateMethodCoverage(modules).Percent); + Assert.Equal(0, summary.CalculateMethodCoverage(modules).AverageModulePercent); + } + + [Fact] + public void TestCalculateLineCoverage_SingleModule() + { + var summary = new CoverageSummary(); + + System.Collections.Generic.KeyValuePair module = _averageCalculationSingleModule.First(); + System.Collections.Generic.KeyValuePair document = module.Value.First(); + System.Collections.Generic.KeyValuePair @class = document.Value.First(); + System.Collections.Generic.KeyValuePair method = @class.Value.First(); + + Assert.Equal(50, summary.CalculateLineCoverage(_averageCalculationSingleModule).AverageModulePercent); + Assert.Equal(50, summary.CalculateLineCoverage(module.Value).Percent); + Assert.Equal(50, summary.CalculateLineCoverage(document.Value).Percent); + Assert.Equal(50, summary.CalculateLineCoverage(@class.Value).Percent); + Assert.Equal(50, summary.CalculateLineCoverage(method.Value.Lines).Percent); + } + + [Fact] + public void TestCalculateLineCoverage_MultiModule() + { + var summary = new CoverageSummary(); + Documents documentsFirstModule = _averageCalculationMultiModule["module"]; + Documents documentsSecondModule = _averageCalculationMultiModule["additionalModule"]; + + Assert.Equal(37.5, summary.CalculateLineCoverage(_averageCalculationMultiModule).AverageModulePercent); + Assert.Equal(50, summary.CalculateLineCoverage(documentsFirstModule.First().Value).Percent); + + Assert.Equal(33.33, summary.CalculateLineCoverage(documentsSecondModule.First().Value.First().Value.ElementAt(0).Value.Lines).Percent); // covered 1 of 3 + Assert.Equal(0, summary.CalculateLineCoverage(documentsSecondModule.First().Value.First().Value.ElementAt(1).Value.Lines).Percent); // covered 0 of 1 + Assert.Equal(25, summary.CalculateLineCoverage(documentsSecondModule.First().Value).Percent); // covered 1 of 4 lines + } + + [Fact] + public void TestCalculateBranchCoverage_SingleModule() + { + var summary = new CoverageSummary(); + + System.Collections.Generic.KeyValuePair module = _averageCalculationSingleModule.First(); + System.Collections.Generic.KeyValuePair document = module.Value.First(); + System.Collections.Generic.KeyValuePair @class = document.Value.First(); + System.Collections.Generic.KeyValuePair method = @class.Value.First(); + + Assert.Equal(100, summary.CalculateBranchCoverage(_averageCalculationSingleModule).AverageModulePercent); + Assert.Equal(100, summary.CalculateBranchCoverage(module.Value).Percent); + Assert.Equal(100, summary.CalculateBranchCoverage(document.Value).Percent); + Assert.Equal(100, summary.CalculateBranchCoverage(@class.Value).Percent); + Assert.Equal(100, summary.CalculateBranchCoverage(method.Value.Branches).Percent); + } + + [Fact] + public void TestCalculateBranchCoverage_MultiModule() + { + var summary = new CoverageSummary(); + Documents documentsFirstModule = _averageCalculationMultiModule["module"]; + Documents documentsSecondModule = _averageCalculationMultiModule["additionalModule"]; + + Assert.Equal(83.33, summary.CalculateBranchCoverage(_averageCalculationMultiModule).AverageModulePercent); + Assert.Equal(100, summary.CalculateBranchCoverage(documentsFirstModule.First().Value).Percent); + Assert.Equal(66.66, summary.CalculateBranchCoverage(documentsSecondModule.First().Value).Percent); + } + + [Fact] + public void TestCalculateMethodCoverage_SingleModule() + { + var summary = new CoverageSummary(); + + System.Collections.Generic.KeyValuePair module = _averageCalculationSingleModule.First(); + System.Collections.Generic.KeyValuePair document = module.Value.First(); + System.Collections.Generic.KeyValuePair @class = document.Value.First(); + System.Collections.Generic.KeyValuePair method = @class.Value.First(); + + Assert.Equal(100, summary.CalculateMethodCoverage(_averageCalculationSingleModule).AverageModulePercent); + Assert.Equal(100, summary.CalculateMethodCoverage(module.Value).Percent); + Assert.Equal(100, summary.CalculateMethodCoverage(document.Value).Percent); + Assert.Equal(100, summary.CalculateMethodCoverage(@class.Value).Percent); + Assert.Equal(100, summary.CalculateMethodCoverage(method.Value.Lines).Percent); + } + + [Fact] + public void TestCalculateMethodCoverage_MultiModule() + { + var summary = new CoverageSummary(); + Documents documentsFirstModule = _averageCalculationMultiModule["module"]; + Documents documentsSecondModule = _averageCalculationMultiModule["additionalModule"]; + + Assert.Equal(75, summary.CalculateMethodCoverage(_averageCalculationMultiModule).AverageModulePercent); + Assert.Equal(100, summary.CalculateMethodCoverage(documentsFirstModule.First().Value).Percent); + Assert.Equal(50, summary.CalculateMethodCoverage(documentsSecondModule.First().Value).Percent); + } + + [Fact] + public void TestCalculateLineCoveragePercentage_ArithmeticPrecisionCheck() + { + var summary = new CoverageSummary(); + + System.Collections.Generic.KeyValuePair module = _moduleArithmeticPrecision.First(); + System.Collections.Generic.KeyValuePair document = module.Value.First(); + System.Collections.Generic.KeyValuePair @class = document.Value.First(); + System.Collections.Generic.KeyValuePair method = @class.Value.First(); + + Assert.Equal(16.66, summary.CalculateLineCoverage(_moduleArithmeticPrecision).AverageModulePercent); + Assert.Equal(16.66, summary.CalculateLineCoverage(module.Value).Percent); + Assert.Equal(16.66, summary.CalculateLineCoverage(document.Value).Percent); + Assert.Equal(16.66, summary.CalculateLineCoverage(@class.Value).Percent); + Assert.Equal(16.66, summary.CalculateLineCoverage(method.Value.Lines).Percent); + } + + [Fact] + public void TestCalculateBranchCoveragePercentage_ArithmeticPrecisionCheck() + { + var summary = new CoverageSummary(); + + System.Collections.Generic.KeyValuePair module = _moduleArithmeticPrecision.First(); + System.Collections.Generic.KeyValuePair document = module.Value.First(); + System.Collections.Generic.KeyValuePair @class = document.Value.First(); + System.Collections.Generic.KeyValuePair method = @class.Value.First(); + + Assert.Equal(16.66, summary.CalculateBranchCoverage(_moduleArithmeticPrecision).AverageModulePercent); + Assert.Equal(16.66, summary.CalculateBranchCoverage(module.Value).Percent); + Assert.Equal(16.66, summary.CalculateBranchCoverage(document.Value).Percent); + Assert.Equal(16.66, summary.CalculateBranchCoverage(@class.Value).Percent); + Assert.Equal(16.66, summary.CalculateBranchCoverage(method.Value.Branches).Percent); + } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs index 4ec8bc7f7..7b6884905 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs @@ -10,204 +10,204 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void AsyncAwait() { - [Fact] - public void AsyncAwait() + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - instance.SyncExecution(); - - int res = await (Task)instance.AsyncExecution(true); - res = await (Task)instance.AsyncExecution(1); - res = await (Task)instance.AsyncExecution(2); - res = await (Task)instance.AsyncExecution(3); - res = await (Task)instance.ContinuationCalled(); - res = await (Task)instance.ConfigureAwait(); - - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); - - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AsyncAwait.cs") - .AssertLinesCovered(BuildConfiguration.Debug, - // AsyncExecution(bool) - (10, 1), (11, 1), (12, 1), (14, 1), (16, 1), (17, 0), (18, 0), (19, 0), (21, 1), (22, 1), - // Async - (25, 9), (26, 9), (27, 9), (28, 9), - // SyncExecution - (31, 1), (32, 1), (33, 1), - // Sync - (36, 1), (37, 1), (38, 1), - // AsyncExecution(int) - (41, 3), (42, 3), (43, 3), (46, 1), (47, 1), (48, 1), (51, 1), - (52, 1), (53, 1), (56, 1), (57, 1), (58, 1), (59, 1), - (62, 0), (63, 0), (64, 0), (65, 0), (68, 0), (70, 3), (71, 3), - // ContinuationNotCalled - (74, 0), (75, 0), (76, 0), (77, 0), (78, 0), - // ContinuationCalled -> line 83 should be 1 hit some issue with Continuation state machine - (81, 1), (82, 1), (83, 2), (84, 1), (85, 1), - // ConfigureAwait - (89, 1), (90, 1) - ) - .AssertBranchesCovered(BuildConfiguration.Debug, (16, 0, 0), (16, 1, 1), (43, 0, 3), (43, 1, 1), (43, 2, 1), (43, 3, 1), (43, 4, 0)) - // Real branch should be 2, we should try to remove compiler generated branch in method ContinuationNotCalled/ContinuationCalled - // for Continuation state machine - .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 2); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void AsyncAwait_Issue_669_1() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + instance.SyncExecution(); + + int res = await (Task)instance.AsyncExecution(true); + res = await (Task)instance.AsyncExecution(1); + res = await (Task)instance.AsyncExecution(2); + res = await (Task)instance.AsyncExecution(3); + res = await (Task)instance.ContinuationCalled(); + res = await (Task)instance.ConfigureAwait(); + + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); + + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AsyncAwait.cs") + .AssertLinesCovered(BuildConfiguration.Debug, + // AsyncExecution(bool) + (10, 1), (11, 1), (12, 1), (14, 1), (16, 1), (17, 0), (18, 0), (19, 0), (21, 1), (22, 1), + // Async + (25, 9), (26, 9), (27, 9), (28, 9), + // SyncExecution + (31, 1), (32, 1), (33, 1), + // Sync + (36, 1), (37, 1), (38, 1), + // AsyncExecution(int) + (41, 3), (42, 3), (43, 3), (46, 1), (47, 1), (48, 1), (51, 1), + (52, 1), (53, 1), (56, 1), (57, 1), (58, 1), (59, 1), + (62, 0), (63, 0), (64, 0), (65, 0), (68, 0), (70, 3), (71, 3), + // ContinuationNotCalled + (74, 0), (75, 0), (76, 0), (77, 0), (78, 0), + // ContinuationCalled -> line 83 should be 1 hit some issue with Continuation state machine + (81, 1), (82, 1), (83, 2), (84, 1), (85, 1), + // ConfigureAwait + (89, 1), (90, 1) + ) + .AssertBranchesCovered(BuildConfiguration.Debug, (16, 0, 0), (16, 1, 1), (43, 0, 3), (43, 1, 1), (43, 2, 1), (43, 3, 1), (43, 4, 0)) + // Real branch should be 2, we should try to remove compiler generated branch in method ContinuationNotCalled/ContinuationCalled + // for Continuation state machine + .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 2); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void AsyncAwait_Issue_669_1() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - await (Task)instance.Test(); - }, - persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AsyncAwait.cs") - .AssertLinesCovered(BuildConfiguration.Debug, - (97, 1), (98, 1), (99, 1), (101, 1), (102, 1), (103, 1), - (110, 1), (111, 1), (112, 1), (113, 1), - (116, 1), (117, 1), (118, 1), (119, 1)); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void AsyncAwait_Issue_669_2() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + await (Task)instance.Test(); + }, + persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AsyncAwait.cs") + .AssertLinesCovered(BuildConfiguration.Debug, + (97, 1), (98, 1), (99, 1), (101, 1), (102, 1), (103, 1), + (110, 1), (111, 1), (112, 1), (113, 1), + (116, 1), (117, 1), (118, 1), (119, 1)); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void AsyncAwait_Issue_669_2() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - await (ValueTask)instance.SendRequest(); - }, - persistPrepareResultToFile: pathSerialize[0], - assemblyLocation: Assembly.GetExecutingAssembly().Location); - - return 0; - }, new string[] { path }); - - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AsyncAwait.cs") - .AssertLinesCovered(BuildConfiguration.Debug, (7, 1), (10, 1), (11, 1), (12, 1), (13, 1), (15, 1)) - .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 0); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void AsyncAwait_Issue_1177() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + await (ValueTask)instance.SendRequest(); + }, + persistPrepareResultToFile: pathSerialize[0], + assemblyLocation: Assembly.GetExecutingAssembly().Location); + + return 0; + }, new string[] { path }); + + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AsyncAwait.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (7, 1), (10, 1), (11, 1), (12, 1), (13, 1), (15, 1)) + .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 0); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void AsyncAwait_Issue_1177() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - await (Task)instance.Test(); - }, - persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - Core.Instrumentation.Document document = TestInstrumentationHelper.GetCoverageResult(path).Document("Instrumentation.AsyncAwait.cs"); - document.AssertLinesCovered(BuildConfiguration.Debug, (133, 1), (134, 1), (135, 1), (136, 1), (137, 1)); - Assert.DoesNotContain(document.Branches, x => x.Key.Line == 134); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void AsyncAwait_Issue_1233() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + await (Task)instance.Test(); + }, + persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + Core.Instrumentation.Document document = TestInstrumentationHelper.GetCoverageResult(path).Document("Instrumentation.AsyncAwait.cs"); + document.AssertLinesCovered(BuildConfiguration.Debug, (133, 1), (134, 1), (135, 1), (136, 1), (137, 1)); + Assert.DoesNotContain(document.Branches, x => x.Key.Line == 134); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void AsyncAwait_Issue_1233() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - await (Task)instance.Test(); - }, - persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - Core.Instrumentation.Document document = TestInstrumentationHelper.GetCoverageResult(path).Document("Instrumentation.AsyncAwait.cs"); - document.AssertLinesCovered(BuildConfiguration.Debug, (150, 1)); - Assert.DoesNotContain(document.Branches, x => x.Key.Line == 150); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void AsyncAwait_Issue_1275() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + await (Task)instance.Test(); + }, + persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + Core.Instrumentation.Document document = TestInstrumentationHelper.GetCoverageResult(path).Document("Instrumentation.AsyncAwait.cs"); + document.AssertLinesCovered(BuildConfiguration.Debug, (150, 1)); + Assert.DoesNotContain(document.Branches, x => x.Key.Line == 150); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void AsyncAwait_Issue_1275() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - var cts = new CancellationTokenSource(); - await (Task)instance.Execute(cts.Token); - }, - persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - Core.Instrumentation.Document document = TestInstrumentationHelper.GetCoverageResult(path).Document("Instrumentation.AsyncAwait.cs"); - document.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 170, 176); - document.AssertBranchesCovered(BuildConfiguration.Debug, (171, 0, 1), (171, 1, 1)); - Assert.DoesNotContain(document.Branches, x => x.Key.Line == 176); - } - finally - { - File.Delete(path); - } - } + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + var cts = new CancellationTokenSource(); + await (Task)instance.Execute(cts.Token); + }, + persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + Core.Instrumentation.Document document = TestInstrumentationHelper.GetCoverageResult(path).Document("Instrumentation.AsyncAwait.cs"); + document.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 170, 176); + document.AssertBranchesCovered(BuildConfiguration.Debug, (171, 0, 1), (171, 1, 1)); + Assert.DoesNotContain(document.Branches, x => x.Key.Line == 176); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwaitValueTask.cs b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwaitValueTask.cs index b45d044ab..e9da72134 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwaitValueTask.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwaitValueTask.cs @@ -8,62 +8,62 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void AsyncAwaitWithValueTask() { - [Fact] - public void AsyncAwaitWithValueTask() + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - instance.SyncExecution(); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + instance.SyncExecution(); - int res = await (ValueTask)instance.AsyncExecution(true); - res = await (ValueTask)instance.AsyncExecution(1); - res = await (ValueTask)instance.AsyncExecution(2); - res = await (ValueTask)instance.AsyncExecution(3); - res = await (ValueTask)instance.ConfigureAwait(); - res = await (Task)instance.WrappingValueTaskAsTask(); - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + int res = await (ValueTask)instance.AsyncExecution(true); + res = await (ValueTask)instance.AsyncExecution(1); + res = await (ValueTask)instance.AsyncExecution(2); + res = await (ValueTask)instance.AsyncExecution(3); + res = await (ValueTask)instance.ConfigureAwait(); + res = await (Task)instance.WrappingValueTaskAsTask(); + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AsyncAwaitValueTask.cs") - .AssertLinesCovered(BuildConfiguration.Debug, - // AsyncExecution(bool) - (12, 1), (13, 1), (15, 1), (16, 1), (18, 1), (19, 1), (21, 1), (23, 1), (24, 0), (25, 0), (26, 0), (28, 1), (29, 1), - // Async - (32, 10), (33, 10), (34, 10), (35, 10), (36, 10), - // AsyncExecution(int) - (39, 3), (40, 3), (42, 3), (43, 3), (45, 3), (46, 3), - (49, 1), (50, 1), (51, 1), (54, 1), (55, 1), (56, 1), (59, 1), (60, 1), (62, 1), - (65, 0), (66, 0), (67, 0), (68, 0), (71, 0), - // SyncExecution - (77, 1), (78, 1), (79, 1), - // Sync - (82, 1), (83, 1), (84, 1), - // ConfigureAwait - (87, 1), (88, 1), (90, 1), (91, 1), (93, 1), (94, 1), (95, 1), - // WrappingValueTaskAsTask - (98, 1), (99, 1), (101, 1), (102, 1), (104, 1), (106, 1), (107, 1) - ) - .AssertBranchesCovered(BuildConfiguration.Debug, - // AsyncExecution(bool) if statement - (23, 0, 0), (23, 1, 1), - // AsyncExecution(int) switch statement - (46, 0, 3), (46, 1, 1), (46, 2, 1), (46, 3, 1), (46, 4, 0) - ) - .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 2); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AsyncAwaitValueTask.cs") + .AssertLinesCovered(BuildConfiguration.Debug, + // AsyncExecution(bool) + (12, 1), (13, 1), (15, 1), (16, 1), (18, 1), (19, 1), (21, 1), (23, 1), (24, 0), (25, 0), (26, 0), (28, 1), (29, 1), + // Async + (32, 10), (33, 10), (34, 10), (35, 10), (36, 10), + // AsyncExecution(int) + (39, 3), (40, 3), (42, 3), (43, 3), (45, 3), (46, 3), + (49, 1), (50, 1), (51, 1), (54, 1), (55, 1), (56, 1), (59, 1), (60, 1), (62, 1), + (65, 0), (66, 0), (67, 0), (68, 0), (71, 0), + // SyncExecution + (77, 1), (78, 1), (79, 1), + // Sync + (82, 1), (83, 1), (84, 1), + // ConfigureAwait + (87, 1), (88, 1), (90, 1), (91, 1), (93, 1), (94, 1), (95, 1), + // WrappingValueTaskAsTask + (98, 1), (99, 1), (101, 1), (102, 1), (104, 1), (106, 1), (107, 1) + ) + .AssertBranchesCovered(BuildConfiguration.Debug, + // AsyncExecution(bool) if statement + (23, 0, 0), (23, 1, 1), + // AsyncExecution(int) switch statement + (46, 0, 3), (46, 1, 1), (46, 2, 1), (46, 3, 1), (46, 4, 0) + ) + .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 2); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncForeach.cs b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncForeach.cs index eb33a388d..e5b9cc2f8 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncForeach.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncForeach.cs @@ -9,61 +9,61 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void AsyncForeach() { - [Fact] - public void AsyncForeach() + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - int res = await (ValueTask)instance.SumWithATwist(AsyncEnumerable.Range(1, 5)); - res += await (ValueTask)instance.Sum(AsyncEnumerable.Range(1, 3)); - res += await (ValueTask)instance.SumEmpty(); - await (ValueTask)instance.GenericAsyncForeach(AsyncEnumerable.Range(1, 3)); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + int res = await (ValueTask)instance.SumWithATwist(AsyncEnumerable.Range(1, 5)); + res += await (ValueTask)instance.Sum(AsyncEnumerable.Range(1, 3)); + res += await (ValueTask)instance.SumEmpty(); + await (ValueTask)instance.GenericAsyncForeach(AsyncEnumerable.Range(1, 3)); - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AsyncForeach.cs") - .AssertLinesCovered(BuildConfiguration.Debug, - // SumWithATwist(IAsyncEnumerable) - // Apparently due to entering and exiting the async state machine, line 17 - // (the top of an "await foreach" loop) is reached three times *plus* twice - // per loop iteration. So, in this case, with five loop iterations, we end - // up with 3 + 5 * 2 = 13 hits. - (14, 1), (15, 1), (17, 13), (18, 5), (19, 5), (20, 5), (21, 5), (22, 5), - (24, 0), (25, 0), (26, 0), (27, 5), (29, 1), (30, 1), - // Sum(IAsyncEnumerable) - (34, 1), (35, 1), (37, 9), (38, 3), (39, 3), (40, 3), (42, 1), (43, 1), - // SumEmpty() - (47, 1), (48, 1), (50, 3), (51, 0), (52, 0), (53, 0), (55, 1), (56, 1), - // GenericAsyncForeach - (59,1), (60, 9), (61, 3), (62, 3), (63, 3), (64, 1) - ) - .AssertBranchesCovered(BuildConfiguration.Debug, - // SumWithATwist(IAsyncEnumerable) - (17, 2, 1), (17, 3, 5), (19, 0, 5), (19, 1, 0), - // Sum(IAsyncEnumerable) - (37, 0, 1), (37, 1, 3), - // SumEmpty() - // If we never entered the loop, that's a branch not taken, which is - // what we want to see. - (50, 0, 1), (50, 1, 0), - (60, 0, 1), (60, 1, 3) - ) - .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 5); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AsyncForeach.cs") + .AssertLinesCovered(BuildConfiguration.Debug, + // SumWithATwist(IAsyncEnumerable) + // Apparently due to entering and exiting the async state machine, line 17 + // (the top of an "await foreach" loop) is reached three times *plus* twice + // per loop iteration. So, in this case, with five loop iterations, we end + // up with 3 + 5 * 2 = 13 hits. + (14, 1), (15, 1), (17, 13), (18, 5), (19, 5), (20, 5), (21, 5), (22, 5), + (24, 0), (25, 0), (26, 0), (27, 5), (29, 1), (30, 1), + // Sum(IAsyncEnumerable) + (34, 1), (35, 1), (37, 9), (38, 3), (39, 3), (40, 3), (42, 1), (43, 1), + // SumEmpty() + (47, 1), (48, 1), (50, 3), (51, 0), (52, 0), (53, 0), (55, 1), (56, 1), + // GenericAsyncForeach + (59, 1), (60, 9), (61, 3), (62, 3), (63, 3), (64, 1) + ) + .AssertBranchesCovered(BuildConfiguration.Debug, + // SumWithATwist(IAsyncEnumerable) + (17, 2, 1), (17, 3, 5), (19, 0, 5), (19, 1, 0), + // Sum(IAsyncEnumerable) + (37, 0, 1), (37, 1, 3), + // SumEmpty() + // If we never entered the loop, that's a branch not taken, which is + // what we want to see. + (50, 0, 1), (50, 1, 0), + (60, 0, 1), (60, 1, 3) + ) + .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 5); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncIterator.cs b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncIterator.cs index 0bf6d99b4..8ddd31e5c 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncIterator.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncIterator.cs @@ -8,44 +8,44 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void AsyncIterator() { - [Fact] - public void AsyncIterator() + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - int res = await (Task)instance.Issue1104_Repro(); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + int res = await (Task)instance.Issue1104_Repro(); - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AsyncIterator.cs") - .AssertLinesCovered(BuildConfiguration.Debug, - // Issue1104_Repro() - (14, 1), (15, 1), (17, 203), (18, 100), (19, 100), (20, 100), (22, 1), (23, 1), - // CreateSequenceAsync() - (26, 1), (27, 202), (28, 100), (29, 100), (30, 100), (31, 100), (32, 1) - ) - .AssertBranchesCovered(BuildConfiguration.Debug, - // Issue1104_Repro(), - (17, 0, 1), (17, 1, 100), - // CreateSequenceAsync() - (27, 0, 1), (27, 1, 100) - ) - .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 2); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AsyncIterator.cs") + .AssertLinesCovered(BuildConfiguration.Debug, + // Issue1104_Repro() + (14, 1), (15, 1), (17, 203), (18, 100), (19, 100), (20, 100), (22, 1), (23, 1), + // CreateSequenceAsync() + (26, 1), (27, 202), (28, 100), (29, 100), (30, 100), (31, 100), (32, 1) + ) + .AssertBranchesCovered(BuildConfiguration.Debug, + // Issue1104_Repro(), + (17, 0, 1), (17, 1, 100), + // CreateSequenceAsync() + (27, 0, 1), (27, 1, 100) + ) + .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 2); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.AutoProps.cs b/test/coverlet.core.tests/Coverage/CoverageTests.AutoProps.cs index 134a61bad..fc3cf6587 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.AutoProps.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.AutoProps.cs @@ -8,144 +8,144 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SkipAutoProps(bool skipAutoProps) { - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SkipAutoProps(bool skipAutoProps) + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] parameters) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] parameters) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.AutoPropsNonInit = 10; - instance.AutoPropsInit = 20; - int readValue = instance.AutoPropsNonInit; - readValue = instance.AutoPropsInit; - return Task.CompletedTask; - }, - persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1])); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.AutoPropsNonInit = 10; + instance.AutoPropsInit = 20; + int readValue = instance.AutoPropsNonInit; + readValue = instance.AutoPropsInit; + return Task.CompletedTask; + }, + persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1])); - return 0; - }, new string[] { path, skipAutoProps.ToString() }); + return 0; + }, new string[] { path, skipAutoProps.ToString() }); - if (skipAutoProps) - { - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AutoProps.cs") - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 12, 13) - .AssertNonInstrumentedLines(BuildConfiguration.Release, 12, 13) - .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 9, 11) - .AssertLinesCovered(BuildConfiguration.Debug, (7, 1)) - .AssertLinesCovered(BuildConfiguration.Release, (10, 1)); - } - else - { - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AutoProps.cs") - .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 7, 13) - .AssertLinesCoveredFromTo(BuildConfiguration.Release, 10, 10) - .AssertLinesCoveredFromTo(BuildConfiguration.Release, 12, 13); - } - } - finally - { - File.Delete(path); - } + if (skipAutoProps) + { + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AutoProps.cs") + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 12, 13) + .AssertNonInstrumentedLines(BuildConfiguration.Release, 12, 13) + .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 9, 11) + .AssertLinesCovered(BuildConfiguration.Debug, (7, 1)) + .AssertLinesCovered(BuildConfiguration.Release, (10, 1)); + } + else + { + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AutoProps.cs") + .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 7, 13) + .AssertLinesCoveredFromTo(BuildConfiguration.Release, 10, 10) + .AssertLinesCoveredFromTo(BuildConfiguration.Release, 12, 13); } + } + finally + { + File.Delete(path); + } + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SkipAutoPropsInRecords(bool skipAutoProps) + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SkipAutoPropsInRecords(bool skipAutoProps) + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] parameters) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] parameters) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.RecordAutoPropsNonInit = string.Empty; - instance.RecordAutoPropsInit = string.Empty; - string readValue = instance.RecordAutoPropsInit; - readValue = instance.RecordAutoPropsNonInit; - return Task.CompletedTask; - }, - persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1])); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.RecordAutoPropsNonInit = string.Empty; + instance.RecordAutoPropsInit = string.Empty; + string readValue = instance.RecordAutoPropsInit; + readValue = instance.RecordAutoPropsNonInit; + return Task.CompletedTask; + }, + persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1])); - return 0; - }, new string[] { path, skipAutoProps.ToString() }); + return 0; + }, new string[] { path, skipAutoProps.ToString() }); - if (skipAutoProps) - { - TestInstrumentationHelper.GetCoverageResult(path).GenerateReport(show: true) - .Document("Instrumentation.AutoProps.cs") - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 23, 24) - .AssertNonInstrumentedLines(BuildConfiguration.Release, 23, 24) - .AssertLinesCovered(BuildConfiguration.Debug, (18, 1), (20, 1), (21, 1), (22, 1)) - .AssertLinesCovered(BuildConfiguration.Release, (21, 1)); - } - else - { - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AutoProps.cs") - .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 18, 24) - .AssertLinesCoveredFromTo(BuildConfiguration.Release, 21, 21) - .AssertLinesCoveredFromTo(BuildConfiguration.Release, 23, 24); - } - } - finally - { - File.Delete(path); - } + if (skipAutoProps) + { + TestInstrumentationHelper.GetCoverageResult(path).GenerateReport(show: true) + .Document("Instrumentation.AutoProps.cs") + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 23, 24) + .AssertNonInstrumentedLines(BuildConfiguration.Release, 23, 24) + .AssertLinesCovered(BuildConfiguration.Debug, (18, 1), (20, 1), (21, 1), (22, 1)) + .AssertLinesCovered(BuildConfiguration.Release, (21, 1)); } + else + { + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AutoProps.cs") + .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 18, 24) + .AssertLinesCoveredFromTo(BuildConfiguration.Release, 21, 21) + .AssertLinesCoveredFromTo(BuildConfiguration.Release, 23, 24); + } + } + finally + { + File.Delete(path); + } + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SkipRecordWithProperties(bool skipAutoProps) + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SkipRecordWithProperties(bool skipAutoProps) + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] parameters) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] parameters) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - return Task.CompletedTask; - }, - persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1])); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + return Task.CompletedTask; + }, + persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1])); - return 0; - }, new string[] { path, skipAutoProps.ToString() }); + return 0; + }, new string[] { path, skipAutoProps.ToString() }); - if (skipAutoProps) - { - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AutoProps.cs") - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 29, 29) - .AssertNonInstrumentedLines(BuildConfiguration.Release, 29, 29) - .AssertLinesCovered(BuildConfiguration.Debug, (32, 1), (33, 1), (34, 1)) - .AssertLinesCovered(BuildConfiguration.Release, (33, 1)); + if (skipAutoProps) + { + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AutoProps.cs") + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 29, 29) + .AssertNonInstrumentedLines(BuildConfiguration.Release, 29, 29) + .AssertLinesCovered(BuildConfiguration.Debug, (32, 1), (33, 1), (34, 1)) + .AssertLinesCovered(BuildConfiguration.Release, (33, 1)); - } - else - { - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AutoProps.cs") - .AssertLinesCovered(BuildConfiguration.Debug, (29, 3), (31, 1), (32, 1), (33, 1), (34, 1)) - .AssertLinesCovered(BuildConfiguration.Release, (29, 3), (31, 1), (33, 1)); - } - } - finally - { - File.Delete(path); - } } + else + { + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AutoProps.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (29, 3), (31, 1), (32, 1), (33, 1), (34, 1)) + .AssertLinesCovered(BuildConfiguration.Release, (29, 3), (31, 1), (33, 1)); + } + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.AwaitUsing.cs b/test/coverlet.core.tests/Coverage/CoverageTests.AwaitUsing.cs index 515ebf3a9..de7f7cf5e 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.AwaitUsing.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.AwaitUsing.cs @@ -8,45 +8,45 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void AwaitUsing() { - [Fact] - public void AwaitUsing() + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - await (ValueTask)instance.HasAwaitUsing(); - await (Task)instance.Issue914_Repro(); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + await (ValueTask)instance.HasAwaitUsing(); + await (Task)instance.Issue914_Repro(); - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.AwaitUsing.cs") - .AssertLinesCovered(BuildConfiguration.Debug, - // HasAwaitUsing() - (13, 1), (14, 1), (15, 1), (16, 1), (17, 1), - // Issue914_Repro() - (21, 1), (22, 1), (23, 1), (24, 1), - // Issue914_Repro_Example1() - (28, 1), (29, 1), (30, 1), - // Issue914_Repro_Example2() - (34, 1), (35, 1), (36, 1), (37, 1), - // MyTransaction.DisposeAsync() - (43, 2), (44, 2), (45, 2) - ) - .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 0); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AwaitUsing.cs") + .AssertLinesCovered(BuildConfiguration.Debug, + // HasAwaitUsing() + (13, 1), (14, 1), (15, 1), (16, 1), (17, 1), + // Issue914_Repro() + (21, 1), (22, 1), (23, 1), (24, 1), + // Issue914_Repro_Example1() + (28, 1), (29, 1), (30, 1), + // Issue914_Repro_Example2() + (34, 1), (35, 1), (36, 1), (37, 1), + // MyTransaction.DisposeAsync() + (43, 2), (44, 2), (45, 2) + ) + .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 0); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.CatchBlock.cs b/test/coverlet.core.tests/Coverage/CoverageTests.CatchBlock.cs index 71dfb4d53..baff74eb3 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.CatchBlock.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.CatchBlock.cs @@ -8,72 +8,72 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void CatchBlock_Issue465() { - [Fact] - public void CatchBlock_Issue465() + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - instance.Test(); - instance.Test_Catch(); - await (Task)instance.TestAsync(); - await(Task)instance.TestAsync_Catch(); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + instance.Test(); + instance.Test_Catch(); + await (Task)instance.TestAsync(); + await (Task)instance.TestAsync_Catch(); - instance.Test(true); - instance.Test_Catch(true); - await (Task)instance.TestAsync(true); - await (Task)instance.TestAsync_Catch(true); + instance.Test(true); + instance.Test_Catch(true); + await (Task)instance.TestAsync(true); + await (Task)instance.TestAsync_Catch(true); - instance.Test(false); - instance.Test_Catch(false); - await (Task)instance.TestAsync(false); - await (Task)instance.TestAsync_Catch(false); + instance.Test(false); + instance.Test_Catch(false); + await (Task)instance.TestAsync(false); + await (Task)instance.TestAsync_Catch(false); - instance.Test_WithTypedCatch(); - instance.Test_Catch_WithTypedCatch(); - await (Task)instance.TestAsync_WithTypedCatch(); - await (Task)instance.TestAsync_Catch_WithTypedCatch(); + instance.Test_WithTypedCatch(); + instance.Test_Catch_WithTypedCatch(); + await (Task)instance.TestAsync_WithTypedCatch(); + await (Task)instance.TestAsync_Catch_WithTypedCatch(); - instance.Test_WithTypedCatch(true); - instance.Test_Catch_WithTypedCatch(true); - await (Task)instance.TestAsync_WithTypedCatch(true); - await (Task)instance.TestAsync_Catch_WithTypedCatch(true); + instance.Test_WithTypedCatch(true); + instance.Test_Catch_WithTypedCatch(true); + await (Task)instance.TestAsync_WithTypedCatch(true); + await (Task)instance.TestAsync_Catch_WithTypedCatch(true); - instance.Test_WithTypedCatch(false); - instance.Test_Catch_WithTypedCatch(false); - await (Task)instance.TestAsync_WithTypedCatch(false); - await (Task)instance.TestAsync_Catch_WithTypedCatch(false); + instance.Test_WithTypedCatch(false); + instance.Test_Catch_WithTypedCatch(false); + await (Task)instance.TestAsync_WithTypedCatch(false); + await (Task)instance.TestAsync_Catch_WithTypedCatch(false); - instance.Test_WithNestedCatch(true); - instance.Test_Catch_WithNestedCatch(true); - await (Task)instance.TestAsync_WithNestedCatch(true); - await (Task)instance.TestAsync_Catch_WithNestedCatch(true); + instance.Test_WithNestedCatch(true); + instance.Test_Catch_WithNestedCatch(true); + await (Task)instance.TestAsync_WithNestedCatch(true); + await (Task)instance.TestAsync_Catch_WithNestedCatch(true); - instance.Test_WithNestedCatch(false); - instance.Test_Catch_WithNestedCatch(false); - await (Task)instance.TestAsync_WithNestedCatch(false); - await (Task)instance.TestAsync_Catch_WithNestedCatch(false); + instance.Test_WithNestedCatch(false); + instance.Test_Catch_WithNestedCatch(false); + await (Task)instance.TestAsync_WithNestedCatch(false); + await (Task)instance.TestAsync_Catch_WithNestedCatch(false); - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); - CoverageResult res = TestInstrumentationHelper.GetCoverageResult(path); - res.Document("Instrumentation.CatchBlock.cs") - .AssertLinesCoveredAllBut(BuildConfiguration.Debug, 45, 59, 113, 127, 137, 138, 139, 153, 154, 155, 156, 175, 189, 199, 200, 201, 222, 223, 224, 225, 252, 266, 335, 349) - .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 6) - .ExpectedTotalNumberOfBranches(BuildConfiguration.Release, 6); - } - finally - { - File.Delete(path); - } - } + CoverageResult res = TestInstrumentationHelper.GetCoverageResult(path); + res.Document("Instrumentation.CatchBlock.cs") + .AssertLinesCoveredAllBut(BuildConfiguration.Debug, 45, 59, 113, 127, 137, 138, 139, 153, 154, 155, 156, 175, 189, 199, 200, 201, 222, 223, 224, 225, 252, 266, 335, 349) + .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 6) + .ExpectedTotalNumberOfBranches(BuildConfiguration.Release, 6); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.DoesNotReturn.cs b/test/coverlet.core.tests/Coverage/CoverageTests.DoesNotReturn.cs index e5950232a..b989fad84 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.DoesNotReturn.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.DoesNotReturn.cs @@ -9,295 +9,295 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + + [Fact] + public void NoBranches_DoesNotReturnAttribute_InstrumentsCorrect() { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => + { + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + try { instance.NoBranches(); } + catch (Exception) { } + return Task.CompletedTask; + + }, persistPrepareResultToFile: pathSerialize[0], doesNotReturnAttributes: _ => new string[] { "DoesNotReturnAttribute" }); + + return 0; + + }, new string[] { path }); - [Fact] - public void NoBranches_DoesNotReturnAttribute_InstrumentsCorrect() + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.DoesNotReturn.cs") + .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 12, 13, 14) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 15, 16); + } + finally + { + File.Delete(path); + } + } + + [Fact(Skip = "Hang due to System.Console.ReadKey()")] + public void If_DoesNotReturnAttribute_InstrumentsCorrect() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - try { instance.NoBranches(); } + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + try { instance.If(); } catch (Exception) { } return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0], doesNotReturnAttributes: _ => new string[] { "DoesNotReturnAttribute" }); + }, persistPrepareResultToFile: pathSerialize[0]); - return 0; + return 0; - }, new string[] { path }); + }, new string[] { path }); - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - result.Document("Instrumentation.DoesNotReturn.cs") - .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 12, 13, 14) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 15, 16); - } - finally - { - File.Delete(path); - } - } + result.Document("Instrumentation.DoesNotReturn.cs") + .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 19, 20, 22, 23, 24, 25, 29, 30) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 26, 27); + } + finally + { + File.Delete(path); + } + } - [Fact(Skip = "Hang due to System.Console.ReadKey()")] - public void If_DoesNotReturnAttribute_InstrumentsCorrect() - { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - try { instance.If(); } - catch (Exception) { } - return Task.CompletedTask; - - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.DoesNotReturn.cs") - .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 19, 20, 22, 23, 24, 25, 29, 30) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 26, 27); - } - finally - { - File.Delete(path); - } - } - - [Fact(Skip = "Hang due to System.Console.ReadKey()")] - public void Switch_DoesNotReturnAttribute_InstrumentsCorrect() + [Fact(Skip = "Hang due to System.Console.ReadKey()")] + public void Switch_DoesNotReturnAttribute_InstrumentsCorrect() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - try { instance.Switch(); } - catch (Exception) { } - return Task.CompletedTask; - - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.DoesNotReturn.cs") - .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 33, 34, 36, 39, 40, 44, 45, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 64, 65, 68, 69) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 41, 42); - } - finally - { - File.Delete(path); - } - } - - [Fact(Skip = "Hang due to System.Console.ReadKey()")] - public void Subtle_DoesNotReturnAttribute_InstrumentsCorrect() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + try { instance.Switch(); } + catch (Exception) { } + return Task.CompletedTask; + + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.DoesNotReturn.cs") + .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 33, 34, 36, 39, 40, 44, 45, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 64, 65, 68, 69) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 41, 42); + } + finally + { + File.Delete(path); + } + } + + [Fact(Skip = "Hang due to System.Console.ReadKey()")] + public void Subtle_DoesNotReturnAttribute_InstrumentsCorrect() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - try { instance.Subtle(); } - catch (Exception) { } - return Task.CompletedTask; - - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.DoesNotReturn.cs") - .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 72, 73, 75, 78, 82, 83, 86, 87, 91, 92, 95, 101, 102, 103) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 79, 80, 88, 96, 98, 99); - } - finally - { - File.Delete(path); - } - } - - [Fact(Skip = "Hang due to System.Console.ReadKey()")] - public void UnreachableBranch_DoesNotReturnAttribute_InstrumentsCorrect() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + try { instance.Subtle(); } + catch (Exception) { } + return Task.CompletedTask; + + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.DoesNotReturn.cs") + .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 72, 73, 75, 78, 82, 83, 86, 87, 91, 92, 95, 101, 102, 103) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 79, 80, 88, 96, 98, 99); + } + finally + { + File.Delete(path); + } + } + + [Fact(Skip = "Hang due to System.Console.ReadKey()")] + public void UnreachableBranch_DoesNotReturnAttribute_InstrumentsCorrect() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - try { instance.UnreachableBranch(); } - catch (Exception) { } - return Task.CompletedTask; - - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.DoesNotReturn.cs") - .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 106, 107, 108) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 110, 111, 112, 113, 114); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void CallsGenericMethodDoesNotReturn_DoesNotReturnAttribute_InstrumentsCorrect() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + try { instance.UnreachableBranch(); } + catch (Exception) { } + return Task.CompletedTask; + + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.DoesNotReturn.cs") + .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 106, 107, 108) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 110, 111, 112, 113, 114); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void CallsGenericMethodDoesNotReturn_DoesNotReturnAttribute_InstrumentsCorrect() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - try { instance.CallsGenericMethodDoesNotReturn(); } - catch (Exception) { } - return Task.CompletedTask; - - }, persistPrepareResultToFile: pathSerialize[0], doesNotReturnAttributes: _ => new string[] { "DoesNotReturnAttribute" }); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.DoesNotReturn.cs") - .AssertInstrumentLines(BuildConfiguration.Debug, 118, 119, 124, 125, 126) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 127, 128); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void CallsGenericClassDoesNotReturn_DoesNotReturnAttribute_InstrumentsCorrect() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + try { instance.CallsGenericMethodDoesNotReturn(); } + catch (Exception) { } + return Task.CompletedTask; + + }, persistPrepareResultToFile: pathSerialize[0], doesNotReturnAttributes: _ => new string[] { "DoesNotReturnAttribute" }); + + return 0; + + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.DoesNotReturn.cs") + .AssertInstrumentLines(BuildConfiguration.Debug, 118, 119, 124, 125, 126) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 127, 128); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void CallsGenericClassDoesNotReturn_DoesNotReturnAttribute_InstrumentsCorrect() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - try { instance.CallsGenericClassDoesNotReturn(); } - catch (Exception) { } - return Task.CompletedTask; - - }, persistPrepareResultToFile: pathSerialize[0], doesNotReturnAttributes: _ => new string[] { "DoesNotReturnAttribute" }); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.DoesNotReturn.cs") - .AssertInstrumentLines(BuildConfiguration.Debug, 134, 135, 140, 141, 142) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 143, 144); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void WithLeave_DoesNotReturnAttribute_InstrumentsCorrect() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + try { instance.CallsGenericClassDoesNotReturn(); } + catch (Exception) { } + return Task.CompletedTask; + + }, persistPrepareResultToFile: pathSerialize[0], doesNotReturnAttributes: _ => new string[] { "DoesNotReturnAttribute" }); + + return 0; + + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.DoesNotReturn.cs") + .AssertInstrumentLines(BuildConfiguration.Debug, 134, 135, 140, 141, 142) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 143, 144); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void WithLeave_DoesNotReturnAttribute_InstrumentsCorrect() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - try { instance.WithLeave(); } - catch (Exception) { } - return Task.CompletedTask; - - }, persistPrepareResultToFile: pathSerialize[0], doesNotReturnAttributes: _ => new string[] { "DoesNotReturnAttribute" }); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.DoesNotReturn.cs") - .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 147, 149, 150, 151, 152, 153, 154, 155, 156, 159, 161, 166, 167, 168) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 163, 164); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void FiltersAndFinallies_DoesNotReturnAttribute_InstrumentsCorrect() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + try { instance.WithLeave(); } + catch (Exception) { } + return Task.CompletedTask; + + }, persistPrepareResultToFile: pathSerialize[0], doesNotReturnAttributes: _ => new string[] { "DoesNotReturnAttribute" }); + + return 0; + + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.DoesNotReturn.cs") + .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 147, 149, 150, 151, 152, 153, 154, 155, 156, 159, 161, 166, 167, 168) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 163, 164); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void FiltersAndFinallies_DoesNotReturnAttribute_InstrumentsCorrect() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - try { instance.FiltersAndFinallies(); } - catch (Exception) { } - return Task.CompletedTask; - - }, persistPrepareResultToFile: pathSerialize[0], doesNotReturnAttributes: _ => new string[] { "DoesNotReturnAttribute" }); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.DoesNotReturn.cs") - .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 171, 173, 174, 175, 179, 180, 181, 182, 185, 186, 187, 188, 192, 193, 194) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 176, 177, 183, 184, 189, 190, 195, 196, 197); - } - finally - { - File.Delete(path); - } - } + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + try { instance.FiltersAndFinallies(); } + catch (Exception) { } + return Task.CompletedTask; + + }, persistPrepareResultToFile: pathSerialize[0], doesNotReturnAttributes: _ => new string[] { "DoesNotReturnAttribute" }); + + return 0; + + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.DoesNotReturn.cs") + .AssertInstrumentLines(BuildConfiguration.Debug, 7, 8, 171, 173, 174, 175, 179, 180, 181, 182, 185, 186, 187, 188, 192, 193, 194) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 176, 177, 183, 184, 189, 190, 195, 196, 197); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.ExcludeFromCoverageAttribute.cs b/test/coverlet.core.tests/Coverage/CoverageTests.ExcludeFromCoverageAttribute.cs index a2f00e987..d8f0800c9 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.ExcludeFromCoverageAttribute.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.ExcludeFromCoverageAttribute.cs @@ -14,293 +14,293 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void TestCoverageSkipModule__AssemblyMarkedAsExcludeFromCodeCoverage() { - [Fact] - public void TestCoverageSkipModule__AssemblyMarkedAsExcludeFromCodeCoverage() - { - var partialMockFileSystem = new Mock(); - partialMockFileSystem.CallBase = true; - partialMockFileSystem.Setup(fs => fs.NewFileStream(It.IsAny(), It.IsAny(), It.IsAny())).Returns((string path, FileMode mode, FileAccess access) => - { - return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - }); - var loggerMock = new Mock(); - - string excludedbyattributeDll = Directory.GetFiles(Path.Combine(Directory.GetCurrentDirectory(), "TestAssets"), "coverlet.tests.projectsample.excludedbyattribute.dll").First(); - - var instrumentationHelper = - new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, - new SourceRootTranslator(excludedbyattributeDll, 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 = true, - SingleHit = false, - MergeWith = string.Empty, - UseSourceLink = false - }; - - // test skip module include test assembly feature - var coverage = new Coverage(excludedbyattributeDll, parameters, loggerMock.Object, instrumentationHelper, partialMockFileSystem.Object, - new SourceRootTranslator(loggerMock.Object, new FileSystem()), new CecilSymbolHelper()); - CoveragePrepareResult result = coverage.PrepareModules(); - Assert.Empty(result.Results); - loggerMock.Verify(l => l.LogVerbose(It.IsAny())); - } - - [Fact] - public void ExcludeFromCodeCoverage_CompilerGeneratedMethodsAndTypes() + var partialMockFileSystem = new Mock(); + partialMockFileSystem.CallBase = true; + partialMockFileSystem.Setup(fs => fs.NewFileStream(It.IsAny(), It.IsAny(), It.IsAny())).Returns((string path, FileMode mode, FileAccess access) => + { + return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + }); + var loggerMock = new Mock(); + + string excludedbyattributeDll = Directory.GetFiles(Path.Combine(Directory.GetCurrentDirectory(), "TestAssets"), "coverlet.tests.projectsample.excludedbyattribute.dll").First(); + + var instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, + new SourceRootTranslator(excludedbyattributeDll, 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 = true, + SingleHit = false, + MergeWith = string.Empty, + UseSourceLink = false + }; + + // test skip module include test assembly feature + var coverage = new Coverage(excludedbyattributeDll, parameters, loggerMock.Object, instrumentationHelper, partialMockFileSystem.Object, + new SourceRootTranslator(loggerMock.Object, new FileSystem()), new CecilSymbolHelper()); + CoveragePrepareResult result = coverage.PrepareModules(); + Assert.Empty(result.Results); + loggerMock.Verify(l => l.LogVerbose(It.IsAny())); + } + + [Fact] + public void ExcludeFromCodeCoverage_CompilerGeneratedMethodsAndTypes() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - await (Task)instance.Test("test"); - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - Core.Instrumentation.Document document = result.Document("Instrumentation.ExcludeFromCoverage.cs"); - - // Invoking method "Test" of class "MethodsWithExcludeFromCodeCoverageAttr" we expect to cover 100% lines for MethodsWithExcludeFromCodeCoverageAttr - Assert.DoesNotContain(document.Lines, l => - (l.Value.Class == "Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr" || - // Compiler generated - l.Value.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr/")) && - l.Value.Hits == 0); - // and 0% for MethodsWithExcludeFromCodeCoverageAttr2 - Assert.DoesNotContain(document.Lines, l => - (l.Value.Class == "Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr2" || - // Compiler generated - l.Value.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr2/")) && - l.Value.Hits == 1); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void ExcludeFromCodeCoverage_CompilerGeneratedMethodsAndTypes_NestedMembers() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + await (Task)instance.Test("test"); + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + Core.Instrumentation.Document document = result.Document("Instrumentation.ExcludeFromCoverage.cs"); + + // Invoking method "Test" of class "MethodsWithExcludeFromCodeCoverageAttr" we expect to cover 100% lines for MethodsWithExcludeFromCodeCoverageAttr + Assert.DoesNotContain(document.Lines, l => + (l.Value.Class == "Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr" || + // Compiler generated + l.Value.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr/")) && + l.Value.Hits == 0); + // and 0% for MethodsWithExcludeFromCodeCoverageAttr2 + Assert.DoesNotContain(document.Lines, l => + (l.Value.Class == "Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr2" || + // Compiler generated + l.Value.Class.StartsWith("Coverlet.Core.Samples.Tests.MethodsWithExcludeFromCodeCoverageAttr2/")) && + l.Value.Hits == 1); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void ExcludeFromCodeCoverage_CompilerGeneratedMethodsAndTypes_NestedMembers() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.Test(); - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.ExcludeFromCoverage.NestedStateMachines.cs") - .AssertLinesCovered(BuildConfiguration.Debug, (14, 1), (15, 1), (16, 1)) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 9, 11); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void ExcludeFromCodeCoverageCompilerGeneratedMethodsAndTypes_Issue670() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.Test(); + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.ExcludeFromCoverage.NestedStateMachines.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (14, 1), (15, 1), (16, 1)) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 9, 11); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void ExcludeFromCodeCoverageCompilerGeneratedMethodsAndTypes_Issue670() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.Test("test"); - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.ExcludeFromCoverage.Issue670.cs") - .AssertLinesCovered(BuildConfiguration.Debug, (8, 1), (9, 1), (10, 1), (11, 1)) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 15, 53); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void ExcludeFromCodeCoverageNextedTypes() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.Test("test"); + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.ExcludeFromCoverage.Issue670.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (8, 1), (9, 1), (10, 1), (11, 1)) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 15, 53); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void ExcludeFromCodeCoverageNextedTypes() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - Assert.Equal(42, instance.Run()); - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - TestInstrumentationHelper.GetCoverageResult(path) - .GenerateReport(show:true) - .Document("Instrumentation.ExcludeFromCoverage.cs") - .AssertLinesCovered(BuildConfiguration.Debug, (145, 1)) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 146, 160); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void ExcludeFromCodeCoverage_Issue809() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + Assert.Equal(42, instance.Run()); + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + TestInstrumentationHelper.GetCoverageResult(path) + .GenerateReport(show: true) + .Document("Instrumentation.ExcludeFromCoverage.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (145, 1)) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 146, 160); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void ExcludeFromCodeCoverage_Issue809() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - Assert.True(await((Task)instance.EditTask(null, 10))); - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.ExcludeFromCoverage.Issue809.cs") - - // public async Task EditTask(Tasks_Issue809 tasks, int val) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 153, 162) - // .AssertNonInstrumentedLines(BuildConfiguration.Debug, 167, 170) -> Shoud be not covered, issue with lambda - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 167, 197) - - // public List GetAllTasks() - // .AssertNonInstrumentedLines(BuildConfiguration.Debug, 263, 266) -> Shoud be not covered, issue with lambda - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 263, 264); - // .AssertNonInstrumentedLines(BuildConfiguration.Debug, 269, 275) -> Shoud be not covered, issue with lambda - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void ExcludeFromCodeCoverageAutoGeneratedGetSet() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + Assert.True(await ((Task)instance.EditTask(null, 10))); + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.ExcludeFromCoverage.Issue809.cs") + + // public async Task EditTask(Tasks_Issue809 tasks, int val) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 153, 162) + // .AssertNonInstrumentedLines(BuildConfiguration.Debug, 167, 170) -> Shoud be not covered, issue with lambda + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 167, 197) + + // public List GetAllTasks() + // .AssertNonInstrumentedLines(BuildConfiguration.Debug, 263, 266) -> Shoud be not covered, issue with lambda + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 263, 264); + // .AssertNonInstrumentedLines(BuildConfiguration.Debug, 269, 275) -> Shoud be not covered, issue with lambda + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void ExcludeFromCodeCoverageAutoGeneratedGetSet() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.SetId(10); - Assert.Equal(10, instance.Id); - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.ExcludeFromCoverage.cs") - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 167) - .AssertLinesCovered(BuildConfiguration.Debug, 169); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void ExcludeFromCodeCoverageAutoGeneratedGet() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.SetId(10); + Assert.Equal(10, instance.Id); + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.ExcludeFromCoverage.cs") + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 167) + .AssertLinesCovered(BuildConfiguration.Debug, 169); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void ExcludeFromCodeCoverageAutoGeneratedGet() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.SetId(10); - Assert.Equal(10, instance.Id); - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.ExcludeFromCoverage.cs") - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 177) - .AssertLinesCovered(BuildConfiguration.Debug, 178, 181); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void ExcludeFromCodeCoverage_Issue1302() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.SetId(10); + Assert.Equal(10, instance.Id); + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.ExcludeFromCoverage.cs") + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 177) + .AssertLinesCovered(BuildConfiguration.Debug, 178, 181); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void ExcludeFromCodeCoverage_Issue1302() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.Run(); - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.ExcludeFromCoverage.Issue1302.cs") - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 10, 13); - } - finally - { - File.Delete(path); - } - } + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.Run(); + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.ExcludeFromCoverage.Issue1302.cs") + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 10, 13); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.Filters.cs b/test/coverlet.core.tests/Coverage/CoverageTests.Filters.cs index e86e61a3f..c2c1ecac8 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.Filters.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.Filters.cs @@ -10,123 +10,123 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void ExcludeFilteredNestedAutogeneratedTypes() { - [Fact] - public void ExcludeFilteredNestedAutogeneratedTypes() + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.Run(); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.Run(); - PropertyInfo stateProp = null; - foreach (Type type in ((Type)instance.GetType()).Assembly.GetTypes()) - { - if (typeof(Issue_689).FullName == type.FullName) - { - Assert.Equal(0, (stateProp = type.GetProperty("State")).GetValue(null)); - break; - } - } + PropertyInfo stateProp = null; + foreach (Type type in ((Type)instance.GetType()).Assembly.GetTypes()) + { + if (typeof(Issue_689).FullName == type.FullName) + { + Assert.Equal(0, (stateProp = type.GetProperty("State")).GetValue(null)); + break; + } + } - foreach (Type type in ((Type)instance.GetType()).Assembly.GetTypes()) - { - if (typeof(EventSource_Issue_689).FullName == type.FullName) - { - type.GetMethod("RaiseEvent").Invoke(null, null); - break; - } - } + foreach (Type type in ((Type)instance.GetType()).Assembly.GetTypes()) + { + if (typeof(EventSource_Issue_689).FullName == type.FullName) + { + type.GetMethod("RaiseEvent").Invoke(null, null); + break; + } + } - Assert.Equal(2, stateProp.GetValue(null)); + Assert.Equal(2, stateProp.GetValue(null)); - return Task.CompletedTask; - }, - includeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*ExcludeFilterNestedAutogeneratedTypes", $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*Issue_689" }, - excludeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*NestedToFilterOut", $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*Uncoverlet" }, - persistPrepareResultToFile: pathSerialize[0]); + return Task.CompletedTask; + }, + includeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*ExcludeFilterNestedAutogeneratedTypes", $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*Issue_689" }, + excludeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*NestedToFilterOut", $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*Uncoverlet" }, + persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.ExcludeFilter.cs") - .AssertLinesCovered(BuildConfiguration.Debug, (12, 1), (13, 1), (14, 1)) - .AssertLinesCovered(BuildConfiguration.Debug, (27, 1), (28, 1), (29, 1), (30, 1), (31, 1)) - .AssertLinesCovered(BuildConfiguration.Debug, (39, 2), (40, 2), (41, 2), (43, 5)) - .AssertLinesCovered(BuildConfiguration.Debug, (50, 1), (51, 1), (52, 1)) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 17, 21) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 33, 36); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.ExcludeFilter.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (12, 1), (13, 1), (14, 1)) + .AssertLinesCovered(BuildConfiguration.Debug, (27, 1), (28, 1), (29, 1), (30, 1), (31, 1)) + .AssertLinesCovered(BuildConfiguration.Debug, (39, 2), (40, 2), (41, 2), (43, 5)) + .AssertLinesCovered(BuildConfiguration.Debug, (50, 1), (51, 1), (52, 1)) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 17, 21) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 33, 36); + } + finally + { + File.Delete(path); + } + } - [Fact] - public void ExcludeFilteredTypes() + [Fact] + public void ExcludeFilteredTypes() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - Assert.Equal(42, instance.Run()); - return Task.CompletedTask; - }, - excludeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*ExcludeFilterOuterTypes" }, - persistPrepareResultToFile: pathSerialize[0]); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + Assert.Equal(42, instance.Run()); + return Task.CompletedTask; + }, + excludeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*ExcludeFilterOuterTypes" }, + persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.ExcludeFilter.cs") - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 1, 62) - .AssertLinesCovered(BuildConfiguration.Debug, (66, 1), (68, 1)); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.ExcludeFilter.cs") + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 1, 62) + .AssertLinesCovered(BuildConfiguration.Debug, (66, 1), (68, 1)); + } + finally + { + File.Delete(path); + } + } - [Fact] - public void ExcludeFilteredNestedTypes() + [Fact] + public void ExcludeFilteredNestedTypes() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - Assert.Equal(42, instance.Run()); - return Task.CompletedTask; - }, - excludeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*ExcludeFilterClass2" }, - persistPrepareResultToFile: pathSerialize[0]); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + Assert.Equal(42, instance.Run()); + return Task.CompletedTask; + }, + excludeFilter: moduleFileName => new string[] { $"[{Path.GetFileNameWithoutExtension(moduleFileName)}*]*ExcludeFilterClass2" }, + persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.ExcludeFilter.cs") - .AssertLinesCovered(BuildConfiguration.Debug, (73, 1)) - .AssertNonInstrumentedLines(BuildConfiguration.Debug, 75, 93); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.ExcludeFilter.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (73, 1)) + .AssertNonInstrumentedLines(BuildConfiguration.Debug, 75, 93); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.GenericAsyncIterator.cs b/test/coverlet.core.tests/Coverage/CoverageTests.GenericAsyncIterator.cs index f9136c5e1..40389372b 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.GenericAsyncIterator.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.GenericAsyncIterator.cs @@ -9,32 +9,32 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void GenericAsyncIterator() { - [Fact] - public void GenericAsyncIterator() + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run>(async instance => - { - List res = await (Task>)instance.Issue1383(); - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run>(async instance => + { + List res = await (Task>)instance.Issue1383(); + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.GenericAsyncIterator.cs") - .AssertLinesCovered(BuildConfiguration.Debug, (13, 1), (14, 1), (20, 1), (21, 1), (22, 1)) - .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 0); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.GenericAsyncIterator.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (13, 1), (14, 1), (20, 1), (21, 1), (22, 1)) + .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 0); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.IntegerOverflow.cs b/test/coverlet.core.tests/Coverage/CoverageTests.IntegerOverflow.cs index 5e206fa34..6f6657c06 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.IntegerOverflow.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.IntegerOverflow.cs @@ -9,60 +9,60 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void CoverageResult_NegativeLineCoverage_TranslatedToMaxValueOfInt32() { - [Fact] - public void CoverageResult_NegativeLineCoverage_TranslatedToMaxValueOfInt32() - { - var instrumenterResult = new InstrumenterResult - { - HitsFilePath = "HitsFilePath", - SourceLink = "SourceLink", - ModulePath = "ModulePath" - }; + var instrumenterResult = new InstrumenterResult + { + HitsFilePath = "HitsFilePath", + SourceLink = "SourceLink", + ModulePath = "ModulePath" + }; - instrumenterResult.HitCandidates.Add(new HitCandidate(false, 0, 1, 1)); + instrumenterResult.HitCandidates.Add(new HitCandidate(false, 0, 1, 1)); - var document = new Document - { - Index = 0, - Path = "Path0" - }; + var document = new Document + { + Index = 0, + Path = "Path0" + }; - document.Lines.Add(1, new Line - { - Class = "Class0", - Hits = 0, - Method = "Method0", - Number = 1 - }); + document.Lines.Add(1, new Line + { + Class = "Class0", + Hits = 0, + Method = "Method0", + Number = 1 + }); - instrumenterResult.Documents.Add("document", document); + instrumenterResult.Documents.Add("document", document); - var coveragePrepareResult = new CoveragePrepareResult - { - UseSourceLink = true, - Results = new[] {instrumenterResult}, - Parameters = new CoverageParameters() - }; + var coveragePrepareResult = new CoveragePrepareResult + { + UseSourceLink = true, + Results = new[] { instrumenterResult }, + Parameters = new CoverageParameters() + }; - Stream memoryStream = new MemoryStream(); - var binaryWriter = new BinaryWriter(memoryStream); - binaryWriter.Write(1); - binaryWriter.Write(-1); - memoryStream.Position = 0; + Stream memoryStream = new MemoryStream(); + var binaryWriter = new BinaryWriter(memoryStream); + binaryWriter.Write(1); + binaryWriter.Write(-1); + memoryStream.Position = 0; - var fileSystemMock = new Mock(); - fileSystemMock.Setup(x => x.Exists(It.IsAny())).Returns(true); - fileSystemMock.Setup(x => x.NewFileStream(It.IsAny(), FileMode.Open, FileAccess.Read)) - .Returns(memoryStream); + var fileSystemMock = new Mock(); + fileSystemMock.Setup(x => x.Exists(It.IsAny())).Returns(true); + fileSystemMock.Setup(x => x.NewFileStream(It.IsAny(), FileMode.Open, FileAccess.Read)) + .Returns(memoryStream); - var coverage = new Coverage(coveragePrepareResult, new Mock().Object, new Mock().Object, - fileSystemMock.Object, new Mock().Object); + var coverage = new Coverage(coveragePrepareResult, new Mock().Object, new Mock().Object, + fileSystemMock.Object, new Mock().Object); - CoverageResult coverageResult = coverage.GetCoverageResult(); - coverageResult.Document("document").AssertLinesCovered(BuildConfiguration.Debug, (1, int.MaxValue)); + CoverageResult coverageResult = coverage.GetCoverageResult(); + coverageResult.Document("document").AssertLinesCovered(BuildConfiguration.Debug, (1, int.MaxValue)); - } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.Lambda.cs b/test/coverlet.core.tests/Coverage/CoverageTests.Lambda.cs index 0121359f4..2f4aa5fa3 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.Lambda.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.Lambda.cs @@ -8,131 +8,131 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + [Fact] + public void Lambda_Issue343() { - [Fact] - public void Lambda_Issue343() + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - instance.InvokeAnonymous_Test(); - await (Task)instance.InvokeAnonymousAsync_Test(); - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + instance.InvokeAnonymous_Test(); + await (Task)instance.InvokeAnonymousAsync_Test(); + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.Lambda.cs") - .AssertLinesCoveredAllBut(BuildConfiguration.Debug, 24, 52) - .AssertBranchesCovered(BuildConfiguration.Debug, - // Expected branches - (23, 0, 0), - (23, 1, 1), - (51, 0, 0), - (51, 1, 1) - ); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.Lambda.cs") + .AssertLinesCoveredAllBut(BuildConfiguration.Debug, 24, 52) + .AssertBranchesCovered(BuildConfiguration.Debug, + // Expected branches + (23, 0, 0), + (23, 1, 1), + (51, 0, 0), + (51, 1, 1) + ); + } + finally + { + File.Delete(path); + } + } - [Fact] - public void AsyncAwait_Issue_730() + [Fact] + public void AsyncAwait_Issue_730() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - await (Task)instance.Invoke(); - }, - persistPrepareResultToFile: pathSerialize[0]); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + await (Task)instance.Invoke(); + }, + persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.Lambda.cs") - .AssertLinesCovered(BuildConfiguration.Debug, (73, 1), (74, 1), (75, 101), (76, 1), (77, 1)) - .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 0); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.Lambda.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (73, 1), (74, 1), (75, 101), (76, 1), (77, 1)) + .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 0); + } + finally + { + File.Delete(path); + } + } - [Fact] - public void Lambda_Issue760() + [Fact] + public void Lambda_Issue760() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => - { - await (Task)instance.If(); - await(Task)instance.Foreach(); - }, - persistPrepareResultToFile: pathSerialize[0]); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + await (Task)instance.If(); + await (Task)instance.Foreach(); + }, + persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.Lambda.cs") - .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 83, 92) - .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 95, 104); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.Lambda.cs") + .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 83, 92) + .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 95, 104); + } + finally + { + File.Delete(path); + } + } - [Fact] - public void Issue_1056() + [Fact] + public void Issue_1056() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.T1(); - return Task.CompletedTask; - }, - persistPrepareResultToFile: pathSerialize[0]); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.T1(); + return Task.CompletedTask; + }, + persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.Lambda.cs") - .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 110, 119) - .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 122, 124) - .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 127, 129) - .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 131, 131) - .AssertLinesCovered(BuildConfiguration.Debug, (110, 1), (111, 2), (112, 2), (113, 2), (114, 2), (115, 2), (116, 2), (117, 2), (118, 2), (119, 1), - (122, 2), (123, 2), (124, 2), - (127, 2), (128, 2), (129, 2), - (131, 4)); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.Lambda.cs") + .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 110, 119) + .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 122, 124) + .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 127, 129) + .AssertLinesCoveredFromTo(BuildConfiguration.Debug, 131, 131) + .AssertLinesCovered(BuildConfiguration.Debug, (110, 1), (111, 2), (112, 2), (113, 2), (114, 2), (115, 2), (116, 2), (117, 2), (118, 2), (119, 1), + (122, 2), (123, 2), (124, 2), + (127, 2), (128, 2), (129, 2), + (131, 4)); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.SelectionStatements.cs b/test/coverlet.core.tests/Coverage/CoverageTests.SelectionStatements.cs index 265b00b8b..a0e058fe3 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.SelectionStatements.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.SelectionStatements.cs @@ -9,146 +9,146 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests : ExternalProcessExecutionTest + public partial class CoverageTests : ExternalProcessExecutionTest + { + [Fact] + public void SelectionStatements_If() { - [Fact] - public void SelectionStatements_If() + // We need to pass file name to remote process where it save instrumentation result + // Similar to msbuild input/output + string path = Path.GetTempFileName(); + try + { + // Lambda will run in a custom process to avoid issue with statics and file locking + FunctionExecutor.Run(async (string[] pathSerialize) => { - // We need to pass file name to remote process where it save instrumentation result - // Similar to msbuild input/output - string path = Path.GetTempFileName(); - try - { - // Lambda will run in a custom process to avoid issue with statics and file locking - FunctionExecutor.Run(async (string[] pathSerialize) => - { - // Run load and call a delegate passing class as dynamic to simplify method call - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - // We call method to trigger coverage hits - instance.If(true); + // Run load and call a delegate passing class as dynamic to simplify method call + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + // We call method to trigger coverage hits + instance.If(true); - // For now we have only async Run helper - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); + // For now we have only async Run helper + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); - // we return 0 if we return something different assert fail - return 0; - }, new string[] { path }); + // we return 0 if we return something different assert fail + return 0; + }, new string[] { path }); - // We retrieve and load CoveragePrepareResult and run coverage calculation - // Similar to msbuild coverage result task - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + // We retrieve and load CoveragePrepareResult and run coverage calculation + // Similar to msbuild coverage result task + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - // Generate html report to check - // TestInstrumentationHelper.GenerateHtmlReport(result); + // Generate html report to check + // TestInstrumentationHelper.GenerateHtmlReport(result); - // Asserts on doc/lines/branches - result.Document("Instrumentation.SelectionStatements.cs") - // (line, hits) - .AssertLinesCovered((11, 1), (15, 0)) - // (line,ordinal,hits) - .AssertBranchesCovered((9, 0, 1), (9, 1, 0)); - } - finally - { - // Cleanup tmp file - File.Delete(path); - } - } + // Asserts on doc/lines/branches + result.Document("Instrumentation.SelectionStatements.cs") + // (line, hits) + .AssertLinesCovered((11, 1), (15, 0)) + // (line,ordinal,hits) + .AssertBranchesCovered((9, 0, 1), (9, 1, 0)); + } + finally + { + // Cleanup tmp file + File.Delete(path); + } + } - [Fact] - public void SelectionStatements_Switch() + [Fact] + public void SelectionStatements_Switch() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.Switch(1); - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.Switch(1); + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - result.Document("Instrumentation.SelectionStatements.cs") - .AssertLinesCovered(BuildConfiguration.Release, (24, 1), (26, 0), (28, 0)) - .AssertBranchesCovered(BuildConfiguration.Release, (24, 1, 1)) - .AssertLinesCovered(BuildConfiguration.Debug, (20, 1), (21, 1), (24, 1), (30, 1)) - .AssertBranchesCovered(BuildConfiguration.Debug, (21, 0, 0), (21, 1, 1), (21, 2, 0), (21, 3, 0)); - } - finally - { - File.Delete(path); - } - } + result.Document("Instrumentation.SelectionStatements.cs") + .AssertLinesCovered(BuildConfiguration.Release, (24, 1), (26, 0), (28, 0)) + .AssertBranchesCovered(BuildConfiguration.Release, (24, 1, 1)) + .AssertLinesCovered(BuildConfiguration.Debug, (20, 1), (21, 1), (24, 1), (30, 1)) + .AssertBranchesCovered(BuildConfiguration.Debug, (21, 0, 0), (21, 1, 1), (21, 2, 0), (21, 3, 0)); + } + finally + { + File.Delete(path); + } + } - [Fact] - public void SelectionStatements_Switch_CSharp8_OneBranch() + [Fact] + public void SelectionStatements_Switch_CSharp8_OneBranch() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - instance.SwitchCsharp8(int.MaxValue); - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.SwitchCsharp8(int.MaxValue); + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.SelectionStatements.cs") - .AssertLinesCovered(BuildConfiguration.Debug, 33, 34, 35, 36, 40) - .AssertLinesNotCovered(BuildConfiguration.Debug, 37, 38, 39) - .AssertBranchesCovered(BuildConfiguration.Debug, (34, 0, 1), (34, 1, 0), (34, 2, 0), (34, 3, 0), (34, 4, 0), (34, 5, 0)) - .ExpectedTotalNumberOfBranches(3); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.SelectionStatements.cs") + .AssertLinesCovered(BuildConfiguration.Debug, 33, 34, 35, 36, 40) + .AssertLinesNotCovered(BuildConfiguration.Debug, 37, 38, 39) + .AssertBranchesCovered(BuildConfiguration.Debug, (34, 0, 1), (34, 1, 0), (34, 2, 0), (34, 3, 0), (34, 4, 0), (34, 5, 0)) + .ExpectedTotalNumberOfBranches(3); + } + finally + { + File.Delete(path); + } + } - [Fact] - public void SelectionStatements_Switch_CSharp8_AllBranches() + [Fact] + public void SelectionStatements_Switch_CSharp8_AllBranches() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + instance.SwitchCsharp8(int.MaxValue); + instance.SwitchCsharp8(uint.MaxValue); + instance.SwitchCsharp8(short.MaxValue); + try { - instance.SwitchCsharp8(int.MaxValue); - instance.SwitchCsharp8(uint.MaxValue); - instance.SwitchCsharp8(short.MaxValue); - try - { - instance.SwitchCsharp8(""); - } - catch { } - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); + instance.SwitchCsharp8(""); + } + catch { } + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); - TestInstrumentationHelper.GetCoverageResult(path) - .Document("Instrumentation.SelectionStatements.cs") - .AssertLinesCovered(BuildConfiguration.Debug, 33, 34, 35, 36, 37, 38, 39, 40) - .AssertBranchesCovered(BuildConfiguration.Debug, (34, 0, 1), (34, 1, 3), (34, 2, 1), (34, 3, 2), (34, 4, 1), (34, 5, 1)) - .ExpectedTotalNumberOfBranches(3); - } - finally - { - File.Delete(path); - } - } + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.SelectionStatements.cs") + .AssertLinesCovered(BuildConfiguration.Debug, 33, 34, 35, 36, 37, 38, 39, 40) + .AssertBranchesCovered(BuildConfiguration.Debug, (34, 0, 1), (34, 1, 3), (34, 2, 1), (34, 3, 2), (34, 4, 1), (34, 5, 1)) + .ExpectedTotalNumberOfBranches(3); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.Yield.cs b/test/coverlet.core.tests/Coverage/CoverageTests.Yield.cs index e491d9003..3cbf0a252 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.Yield.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.Yield.cs @@ -9,158 +9,158 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests : ExternalProcessExecutionTest + public partial class CoverageTests : ExternalProcessExecutionTest + { + [Fact] + public void Yield_Single() { - [Fact] - public void Yield_Single() + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - foreach (dynamic _ in instance.One()) ; - - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.Yield.cs") - .Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/d__0::MoveNext()") - .AssertLinesCovered((9, 1)) - .ExpectedTotalNumberOfBranches(0); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void Yield_Two() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + foreach (dynamic _ in instance.One()) ; + + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.Yield.cs") + .Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/d__0::MoveNext()") + .AssertLinesCovered((9, 1)) + .ExpectedTotalNumberOfBranches(0); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void Yield_Two() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - foreach (dynamic _ in instance.Two()) ; - - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.Yield.cs") - .Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/d__1::MoveNext()") - .AssertLinesCovered((14, 1), (15, 1)) - .ExpectedTotalNumberOfBranches(0); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void Yield_SingleWithSwitch() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + foreach (dynamic _ in instance.Two()) ; + + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.Yield.cs") + .Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/d__1::MoveNext()") + .AssertLinesCovered((14, 1), (15, 1)) + .ExpectedTotalNumberOfBranches(0); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void Yield_SingleWithSwitch() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - foreach (dynamic _ in instance.OneWithSwitch(2)) ; - - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - - return 0; - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.Yield.cs") - .Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/d__2::MoveNext()") - .AssertLinesCovered(BuildConfiguration.Debug, (21, 1), (30, 1), (31, 1), (37, 1)) - .ExpectedTotalNumberOfBranches(1); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void Yield_Three() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + foreach (dynamic _ in instance.OneWithSwitch(2)) ; + + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.Yield.cs") + .Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/d__2::MoveNext()") + .AssertLinesCovered(BuildConfiguration.Debug, (21, 1), (30, 1), (31, 1), (37, 1)) + .ExpectedTotalNumberOfBranches(1); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void Yield_Three() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - foreach (dynamic _ in instance.Three()) ; - - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.Yield.cs") - .Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/d__3::MoveNext()") - .AssertLinesCovered((42, 1), (43, 1), (44, 1)) - .ExpectedTotalNumberOfBranches(0); - } - finally - { - File.Delete(path); - } - } - - [Fact] - public void Yield_Enumerable() + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + foreach (dynamic _ in instance.Three()) ; + + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.Yield.cs") + .Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/d__3::MoveNext()") + .AssertLinesCovered((42, 1), (43, 1), (44, 1)) + .ExpectedTotalNumberOfBranches(0); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void Yield_Enumerable() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => { - string path = Path.GetTempFileName(); - try - { - FunctionExecutor.Run(async (string[] pathSerialize) => - { - CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => - { - foreach (dynamic _ in instance.Enumerable(new[] { "one", "two", "three", "four" })) ; - - return Task.CompletedTask; - }, persistPrepareResultToFile: pathSerialize[0]); - return 0; - }, new string[] { path }); - - CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); - - result.Document("Instrumentation.Yield.cs") - .Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/d__4::MoveNext()") - .AssertLinesCovered(BuildConfiguration.Debug, (48, 1), (49, 1), (50, 4), (51, 5), (52, 1), (54, 4), (55, 4), (56, 4), (57, 1)) - .ExpectedTotalNumberOfBranches(1); - } - finally - { - File.Delete(path); - } - } + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + foreach (dynamic _ in instance.Enumerable(new[] { "one", "two", "three", "four" })) ; + + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); + + CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path); + + result.Document("Instrumentation.Yield.cs") + .Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/d__4::MoveNext()") + .AssertLinesCovered(BuildConfiguration.Debug, (48, 1), (49, 1), (50, 4), (51, 5), (52, 1), (54, 4), (55, 4), (56, 4), (57, 1)) + .ExpectedTotalNumberOfBranches(1); + } + finally + { + File.Delete(path); + } } + } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.cs b/test/coverlet.core.tests/Coverage/CoverageTests.cs index 4f0465fe2..ccd293760 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.cs @@ -11,86 +11,86 @@ namespace Coverlet.Core.Tests { - public partial class CoverageTests + public partial class CoverageTests + { + private readonly Mock _mockLogger = new(); + + [Fact] + public void TestCoverage() + { + string module = GetType().Assembly.Location; + string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb"); + + DirectoryInfo 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 + }; + + var 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(); + + CoverageResult result = coverage.GetCoverageResult(); + + Assert.Empty(result.Modules); + + directory.Delete(true); + } + + [Fact] + public void TestCoverageWithTestAssembly() { - private readonly Mock _mockLogger = new(); - - [Fact] - public void TestCoverage() - { - string module = GetType().Assembly.Location; - string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb"); - - DirectoryInfo 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 - }; - - var 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(); - - CoverageResult result = coverage.GetCoverageResult(); - - Assert.Empty(result.Modules); - - directory.Delete(true); - } - - [Fact] - public void TestCoverageWithTestAssembly() - { - string module = GetType().Assembly.Location; - string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb"); - - DirectoryInfo 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); - - 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 = Array.Empty(), - IncludeDirectories = Array.Empty(), - ExcludeFilters = Array.Empty(), - ExcludedSourceFiles = Array.Empty(), - ExcludeAttributes = Array.Empty(), - IncludeTestAssembly = true, - SingleHit = false, - MergeWith = string.Empty, - UseSourceLink = false - }; - - var coverage = new Coverage(Path.Combine(directory.FullName, Path.GetFileName(module)), parameters, _mockLogger.Object, instrumentationHelper, new FileSystem(), - new SourceRootTranslator(module, _mockLogger.Object, new FileSystem(), new AssemblyAdapter()), new CecilSymbolHelper()); - coverage.PrepareModules(); - - CoverageResult result = coverage.GetCoverageResult(); - - Assert.NotEmpty(result.Modules); - - directory.Delete(true); - } + string module = GetType().Assembly.Location; + string pdb = Path.Combine(Path.GetDirectoryName(module), Path.GetFileNameWithoutExtension(module) + ".pdb"); + + DirectoryInfo 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); + + 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 = Array.Empty(), + IncludeDirectories = Array.Empty(), + ExcludeFilters = Array.Empty(), + ExcludedSourceFiles = Array.Empty(), + ExcludeAttributes = Array.Empty(), + IncludeTestAssembly = true, + SingleHit = false, + MergeWith = string.Empty, + UseSourceLink = false + }; + + var coverage = new Coverage(Path.Combine(directory.FullName, Path.GetFileName(module)), parameters, _mockLogger.Object, instrumentationHelper, new FileSystem(), + new SourceRootTranslator(module, _mockLogger.Object, new FileSystem(), new AssemblyAdapter()), new CecilSymbolHelper()); + coverage.PrepareModules(); + + CoverageResult result = coverage.GetCoverageResult(); + + Assert.NotEmpty(result.Modules); + + directory.Delete(true); } + } } diff --git a/test/coverlet.core.tests/Coverage/InstrumenterHelper.Assertions.cs b/test/coverlet.core.tests/Coverage/InstrumenterHelper.Assertions.cs index 24f567312..c85eb07bb 100644 --- a/test/coverlet.core.tests/Coverage/InstrumenterHelper.Assertions.cs +++ b/test/coverlet.core.tests/Coverage/InstrumenterHelper.Assertions.cs @@ -1,7 +1,6 @@ // Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Coverlet.Core.Instrumentation; using System; using System.Collections.Generic; using System.Diagnostics; @@ -9,453 +8,454 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Text; +using Coverlet.Core.Instrumentation; using Xunit.Sdk; namespace Coverlet.Core.Tests { - [Flags] - public enum BuildConfiguration - { - Debug = 1, - Release = 2 - } - - static class TestInstrumentationAssert + [Flags] + public enum BuildConfiguration + { + Debug = 1, + Release = 2 + } + + static class TestInstrumentationAssert + { + public static CoverageResult GenerateReport(this CoverageResult coverageResult, [CallerMemberName] string directory = "", bool show = false) { - public static CoverageResult GenerateReport(this CoverageResult coverageResult, [CallerMemberName] string directory = "", bool show = false) - { - if (coverageResult is null) - { - throw new ArgumentNullException(nameof(coverageResult)); - } + if (coverageResult is null) + { + throw new ArgumentNullException(nameof(coverageResult)); + } - TestInstrumentationHelper.GenerateHtmlReport(coverageResult, directory: directory); + TestInstrumentationHelper.GenerateHtmlReport(coverageResult, directory: directory); - if (show && Debugger.IsAttached) - { - Process.Start("cmd", "/C " + Path.GetFullPath(Path.Combine(directory, "index.htm"))); - } + if (show && Debugger.IsAttached) + { + Process.Start("cmd", "/C " + Path.GetFullPath(Path.Combine(directory, "index.htm"))); + } - return coverageResult; - } + return coverageResult; + } - public static bool IsPresent(this CoverageResult coverageResult, string docName) + public static bool IsPresent(this CoverageResult coverageResult, string docName) + { + if (docName is null) + { + throw new ArgumentNullException(nameof(docName)); + } + + foreach (InstrumenterResult instrumenterResult in coverageResult.InstrumentedResults) + { + foreach (KeyValuePair document in instrumenterResult.Documents) { - if (docName is null) - { - throw new ArgumentNullException(nameof(docName)); - } - - foreach (InstrumenterResult instrumenterResult in coverageResult.InstrumentedResults) - { - foreach (KeyValuePair document in instrumenterResult.Documents) - { - if (Path.GetFileName(document.Key) == docName) - { - return true; - } - } - } - - return false; + if (Path.GetFileName(document.Key) == docName) + { + return true; + } } + } - public static Document Document(this CoverageResult coverageResult, string docName) - { - if (docName is null) - { - throw new ArgumentNullException(nameof(docName)); - } - - foreach (InstrumenterResult instrumenterResult in coverageResult.InstrumentedResults) - { - foreach (KeyValuePair document in instrumenterResult.Documents) - { - if (Path.GetFileName(document.Key) == docName) - { - return document.Value; - } - } - } - - throw new XunitException($"Document not found '{docName}'"); - } + return false; + } - public static Document Method(this Document document, string methodName) + public static Document Document(this CoverageResult coverageResult, string docName) + { + if (docName is null) + { + throw new ArgumentNullException(nameof(docName)); + } + + foreach (InstrumenterResult instrumenterResult in coverageResult.InstrumentedResults) + { + foreach (KeyValuePair document in instrumenterResult.Documents) { - var methodDoc = new Document { Path = document.Path, Index = document.Index }; - - if (!document.Lines.Any() && !document.Branches.Any()) - { - return methodDoc; - } - - if (document.Lines.Values.All(l => l.Method != methodName) && document.Branches.Values.All(l => l.Method != methodName)) - { - IEnumerable methods = document.Lines.Values.Select(l => $"'{l.Method}'") - .Concat(document.Branches.Values.Select(b => $"'{b.Method}'")) - .Distinct(); - throw new XunitException($"Method '{methodName}' not found. Methods in document: {string.Join(", ", methods)}"); - } + if (Path.GetFileName(document.Key) == docName) + { + return document.Value; + } + } + } - foreach (KeyValuePair line in document.Lines.Where(l => l.Value.Method == methodName)) - { - methodDoc.Lines[line.Key] = line.Value; - } + throw new XunitException($"Document not found '{docName}'"); + } - foreach (KeyValuePair branch in document.Branches.Where(b => b.Value.Method == methodName)) - { - methodDoc.Branches[branch.Key] = branch.Value; - } + public static Document Method(this Document document, string methodName) + { + var methodDoc = new Document { Path = document.Path, Index = document.Index }; + + if (!document.Lines.Any() && !document.Branches.Any()) + { + return methodDoc; + } + + if (document.Lines.Values.All(l => l.Method != methodName) && document.Branches.Values.All(l => l.Method != methodName)) + { + IEnumerable methods = document.Lines.Values.Select(l => $"'{l.Method}'") + .Concat(document.Branches.Values.Select(b => $"'{b.Method}'")) + .Distinct(); + throw new XunitException($"Method '{methodName}' not found. Methods in document: {string.Join(", ", methods)}"); + } + + foreach (KeyValuePair line in document.Lines.Where(l => l.Value.Method == methodName)) + { + methodDoc.Lines[line.Key] = line.Value; + } + + foreach (KeyValuePair branch in document.Branches.Where(b => b.Value.Method == methodName)) + { + methodDoc.Branches[branch.Key] = branch.Value; + } + + return methodDoc; + } - return methodDoc; - } + public static Document AssertBranchesCovered(this Document document, params (int line, int ordinal, int hits)[] lines) + { + return AssertBranchesCovered(document, BuildConfiguration.Debug | BuildConfiguration.Release, lines); + } - public static Document AssertBranchesCovered(this Document document, params (int line, int ordinal, int hits)[] lines) - { - return AssertBranchesCovered(document, BuildConfiguration.Debug | BuildConfiguration.Release, lines); - } + public static Document ExpectedTotalNumberOfBranches(this Document document, int totalExpectedBranch) + { + return ExpectedTotalNumberOfBranches(document, BuildConfiguration.Debug | BuildConfiguration.Release, totalExpectedBranch); + } - public static Document ExpectedTotalNumberOfBranches(this Document document, int totalExpectedBranch) - { - return ExpectedTotalNumberOfBranches(document, BuildConfiguration.Debug | BuildConfiguration.Release, totalExpectedBranch); - } + public static Document ExpectedTotalNumberOfBranches(this Document document, BuildConfiguration configuration, int totalExpectedBranch) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } - public static Document ExpectedTotalNumberOfBranches(this Document document, BuildConfiguration configuration, int totalExpectedBranch) - { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } + BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); - BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + if ((buildConfiguration & configuration) != buildConfiguration) + { + return document; + } - if ((buildConfiguration & configuration) != buildConfiguration) - { - return document; - } + int totalBranch = document.Branches.GroupBy(g => g.Key.Line).Count(); - int totalBranch = document.Branches.GroupBy(g => g.Key.Line).Count(); + if (totalBranch != totalExpectedBranch) + { + throw new XunitException($"Expected total branch is '{totalExpectedBranch}', actual '{totalBranch}'"); + } - if (totalBranch != totalExpectedBranch) - { - throw new XunitException($"Expected total branch is '{totalExpectedBranch}', actual '{totalBranch}'"); - } + return document; + } - return document; - } + public static string ToStringBranches(this Document document) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } + + var builder = new StringBuilder(); + foreach (KeyValuePair branch in document.Branches) + { + builder.AppendLine($"({branch.Value.Number}, {branch.Value.Ordinal}, {branch.Value.Hits}),"); + } + return builder.ToString(); + } - public static string ToStringBranches(this Document document) + public static Document AssertBranchesCovered(this Document document, BuildConfiguration configuration, params (int line, int ordinal, int hits)[] lines) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } + + BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + + if ((buildConfiguration & configuration) != buildConfiguration) + { + return document; + } + + var branchesToCover = new List(lines.Select(b => $"[line {b.line} ordinal {b.ordinal}]")); + foreach (KeyValuePair branch in document.Branches) + { + foreach ((int lineToCheck, int ordinalToCheck, int expectedHits) in lines) { - if (document is null) + if (branch.Value.Number == lineToCheck) + { + if (branch.Value.Ordinal == ordinalToCheck) { - throw new ArgumentNullException(nameof(document)); - } + branchesToCover.Remove($"[line {branch.Value.Number} ordinal {branch.Value.Ordinal}]"); - var builder = new StringBuilder(); - foreach (KeyValuePair branch in document.Branches) - { - builder.AppendLine($"({branch.Value.Number}, {branch.Value.Ordinal}, {branch.Value.Hits}),"); + if (branch.Value.Hits != expectedHits) + { + throw new XunitException($"Unexpected hits expected line: {lineToCheck} ordinal {ordinalToCheck} hits: {expectedHits} actual hits: {branch.Value.Hits}"); + } } - return builder.ToString(); + } } + } - public static Document AssertBranchesCovered(this Document document, BuildConfiguration configuration, params (int line, int ordinal, int hits)[] lines) - { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } + if (branchesToCover.Count != 0) + { + throw new XunitException($"Not all requested branch found, {branchesToCover.Select(l => l.ToString()).Aggregate((a, b) => $"{a}, {b}")}"); + } - BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); - - if ((buildConfiguration & configuration) != buildConfiguration) - { - return document; - } + return document; + } - var branchesToCover = new List(lines.Select(b => $"[line {b.line} ordinal {b.ordinal}]")); - foreach (KeyValuePair branch in document.Branches) - { - foreach ((int lineToCheck, int ordinalToCheck, int expectedHits) in lines) - { - if (branch.Value.Number == lineToCheck) - { - if (branch.Value.Ordinal == ordinalToCheck) - { - branchesToCover.Remove($"[line {branch.Value.Number} ordinal {branch.Value.Ordinal}]"); - - if (branch.Value.Hits != expectedHits) - { - throw new XunitException($"Unexpected hits expected line: {lineToCheck} ordinal {ordinalToCheck} hits: {expectedHits} actual hits: {branch.Value.Hits}"); - } - } - } - } - } + public static Document AssertLinesCovered(this Document document, params (int line, int hits)[] lines) + { + return AssertLinesCovered(document, BuildConfiguration.Debug | BuildConfiguration.Release, lines); + } - if (branchesToCover.Count != 0) + public static Document AssertLinesCoveredAllBut(this Document document, BuildConfiguration configuration, params int[] linesNumber) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } + + BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + + if ((buildConfiguration & configuration) != buildConfiguration) + { + return document; + } + + foreach (KeyValuePair line in document.Lines) + { + bool skip = false; + foreach (int number in linesNumber) + { + if (line.Value.Number == number) + { + skip = true; + if (line.Value.Hits > 0) { - throw new XunitException($"Not all requested branch found, {branchesToCover.Select(l => l.ToString()).Aggregate((a, b) => $"{a}, {b}")}"); + throw new XunitException($"Hits not expected for line {line.Value.Number}"); } - - return document; + } } - public static Document AssertLinesCovered(this Document document, params (int line, int hits)[] lines) - { - return AssertLinesCovered(document, BuildConfiguration.Debug | BuildConfiguration.Release, lines); - } + if (skip) + continue; - public static Document AssertLinesCoveredAllBut(this Document document, BuildConfiguration configuration, params int[] linesNumber) + if (line.Value.Hits == 0) { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } - - BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); - - if ((buildConfiguration & configuration) != buildConfiguration) - { - return document; - } + throw new XunitException($"Hits expected for line: {line.Value.Number}"); + } + } - foreach (KeyValuePair line in document.Lines) - { - bool skip = false; - foreach (int number in linesNumber) - { - if (line.Value.Number == number) - { - skip = true; - if (line.Value.Hits > 0) - { - throw new XunitException($"Hits not expected for line {line.Value.Number}"); - } - } - } - - if (skip) - continue; - - if (line.Value.Hits == 0) - { - throw new XunitException($"Hits expected for line: {line.Value.Number}"); - } - } + return document; + } - return document; - } + public static Document AssertLinesCoveredFromTo(this Document document, int from, int to) + { + return AssertLinesCoveredFromTo(document, BuildConfiguration.Debug | BuildConfiguration.Release, from, to); + } - public static Document AssertLinesCoveredFromTo(this Document document, int from, int to) + public static Document AssertLinesCoveredFromTo(this Document document, BuildConfiguration configuration, int from, int to) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } + + BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + + if ((buildConfiguration & configuration) != buildConfiguration) + { + return document; + } + + if (to < from) + { + throw new ArgumentException("to cannot be lower than from"); + } + + var lines = new List(); + foreach (KeyValuePair line in document.Lines) + { + if (line.Value.Number >= from && line.Value.Number <= to && line.Value.Hits > 0) { - return AssertLinesCoveredFromTo(document, BuildConfiguration.Debug | BuildConfiguration.Release, from, to); + lines.Add(line.Value.Number); } + } - public static Document AssertLinesCoveredFromTo(this Document document, BuildConfiguration configuration, int from, int to) - { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } - - BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + if (!lines.OrderBy(l => l).SequenceEqual(Enumerable.Range(from, to - from + 1))) + { + throw new XunitException($"Unexpected lines covered"); + } - if ((buildConfiguration & configuration) != buildConfiguration) - { - return document; - } - - if (to < from) - { - throw new ArgumentException("to cannot be lower than from"); - } - - var lines = new List(); - foreach (KeyValuePair line in document.Lines) - { - if (line.Value.Number >= from && line.Value.Number <= to && line.Value.Hits > 0) - { - lines.Add(line.Value.Number); - } - } - - if (!lines.OrderBy(l => l).SequenceEqual(Enumerable.Range(from, to - from + 1))) - { - throw new XunitException($"Unexpected lines covered"); - } - - return document; - } + return document; + } - public static Document AssertLinesCovered(this Document document, BuildConfiguration configuration, params (int line, int hits)[] lines) + public static Document AssertLinesCovered(this Document document, BuildConfiguration configuration, params (int line, int hits)[] lines) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } + + BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + + if ((buildConfiguration & configuration) != buildConfiguration) + { + return document; + } + + var linesToCover = new List(lines.Select(l => l.line)); + foreach (KeyValuePair line in document.Lines) + { + foreach ((int lineToCheck, int expectedHits) in lines) { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } - - BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); - - if ((buildConfiguration & configuration) != buildConfiguration) - { - return document; - } - - var linesToCover = new List(lines.Select(l => l.line)); - foreach (KeyValuePair line in document.Lines) + if (line.Value.Number == lineToCheck) + { + linesToCover.Remove(line.Value.Number); + if (line.Value.Hits != expectedHits) { - foreach ((int lineToCheck, int expectedHits) in lines) - { - if (line.Value.Number == lineToCheck) - { - linesToCover.Remove(line.Value.Number); - if (line.Value.Hits != expectedHits) - { - throw new XunitException($"Unexpected hits expected line: {lineToCheck} hits: {expectedHits} actual hits: {line.Value.Hits}"); - } - } - } + throw new XunitException($"Unexpected hits expected line: {lineToCheck} hits: {expectedHits} actual hits: {line.Value.Hits}"); } - - if (linesToCover.Count != 0) - { - throw new XunitException($"Not all requested line found, {linesToCover.Select(l => l.ToString()).Aggregate((a, b) => $"{a}, {b}")}"); - } - - return document; + } } + } - public static Document AssertLinesCovered(this Document document, BuildConfiguration configuration, params int[] lines) - { - return AssertLinesCoveredInternal(document, configuration, true, lines); - } + if (linesToCover.Count != 0) + { + throw new XunitException($"Not all requested line found, {linesToCover.Select(l => l.ToString()).Aggregate((a, b) => $"{a}, {b}")}"); + } - public static Document AssertLinesNotCovered(this Document document, BuildConfiguration configuration, params int[] lines) - { - return AssertLinesCoveredInternal(document, configuration, false, lines); - } + return document; + } - private static Document AssertLinesCoveredInternal(this Document document, BuildConfiguration configuration, bool covered, params int[] lines) - { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } + public static Document AssertLinesCovered(this Document document, BuildConfiguration configuration, params int[] lines) + { + return AssertLinesCoveredInternal(document, configuration, true, lines); + } - BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + public static Document AssertLinesNotCovered(this Document document, BuildConfiguration configuration, params int[] lines) + { + return AssertLinesCoveredInternal(document, configuration, false, lines); + } - if ((buildConfiguration & configuration) != buildConfiguration) + private static Document AssertLinesCoveredInternal(this Document document, BuildConfiguration configuration, bool covered, params int[] lines) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } + + BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + + if ((buildConfiguration & configuration) != buildConfiguration) + { + return document; + } + + var linesToCover = new List(lines); + foreach (KeyValuePair line in document.Lines) + { + foreach (int lineToCheck in lines) + { + if (line.Value.Number == lineToCheck) + { + if (covered && line.Value.Hits > 0) { - return document; + linesToCover.Remove(line.Value.Number); } - - var linesToCover = new List(lines); - foreach (KeyValuePair line in document.Lines) + if (!covered && line.Value.Hits == 0) { - foreach (int lineToCheck in lines) - { - if (line.Value.Number == lineToCheck) - { - if (covered && line.Value.Hits > 0) - { - linesToCover.Remove(line.Value.Number); - } - if (!covered && line.Value.Hits == 0) - { - linesToCover.Remove(line.Value.Number); - } - } - } + linesToCover.Remove(line.Value.Number); } + } + } + } - if (linesToCover.Count != 0) - { - throw new XunitException($"Not all requested line found, {linesToCover.Select(l => l.ToString()).Aggregate((a, b) => $"{a}, {b}")}"); - } + if (linesToCover.Count != 0) + { + throw new XunitException($"Not all requested line found, {linesToCover.Select(l => l.ToString()).Aggregate((a, b) => $"{a}, {b}")}"); + } - return document; - } + return document; + } - public static Document AssertNonInstrumentedLines(this Document document, BuildConfiguration configuration, int from, int to) - { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } + public static Document AssertNonInstrumentedLines(this Document document, BuildConfiguration configuration, int from, int to) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } - BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); - if ((buildConfiguration & configuration) != buildConfiguration) - { - return document; - } + if ((buildConfiguration & configuration) != buildConfiguration) + { + return document; + } - int[] lineRange = Enumerable.Range(from, to - from + 1).ToArray(); + int[] lineRange = Enumerable.Range(from, to - from + 1).ToArray(); - return AssertNonInstrumentedLines(document, configuration, lineRange); - } + return AssertNonInstrumentedLines(document, configuration, lineRange); + } - public static Document AssertNonInstrumentedLines(this Document document, BuildConfiguration configuration, params int[] lines) - { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } + public static Document AssertNonInstrumentedLines(this Document document, BuildConfiguration configuration, params int[] lines) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } - BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); - if ((buildConfiguration & configuration) != buildConfiguration) - { - return document; - } + if ((buildConfiguration & configuration) != buildConfiguration) + { + return document; + } - IEnumerable unexpectedlyInstrumented = document.Lines.Select(l => l.Value.Number).Intersect(lines); + IEnumerable unexpectedlyInstrumented = document.Lines.Select(l => l.Value.Number).Intersect(lines); - if (unexpectedlyInstrumented.Any()) - { - throw new XunitException($"Unexpected instrumented lines, '{string.Join(',', unexpectedlyInstrumented)}'"); - } + if (unexpectedlyInstrumented.Any()) + { + throw new XunitException($"Unexpected instrumented lines, '{string.Join(',', unexpectedlyInstrumented)}'"); + } - return document; - } + return document; + } - public static Document AssertInstrumentLines(this Document document, BuildConfiguration configuration, params int[] lines) - { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } + public static Document AssertInstrumentLines(this Document document, BuildConfiguration configuration, params int[] lines) + { + if (document is null) + { + throw new ArgumentNullException(nameof(document)); + } - BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); + BuildConfiguration buildConfiguration = GetAssemblyBuildConfiguration(); - if ((buildConfiguration & configuration) != buildConfiguration) - { - return document; - } + if ((buildConfiguration & configuration) != buildConfiguration) + { + return document; + } - var instrumentedLines = document.Lines.Select(l => l.Value.Number).ToHashSet(); + var instrumentedLines = document.Lines.Select(l => l.Value.Number).ToHashSet(); - IEnumerable missing = lines.Where(l => !instrumentedLines.Contains(l)); + IEnumerable missing = lines.Where(l => !instrumentedLines.Contains(l)); - if (missing.Any()) - { - throw new XunitException($"Expected lines to be instrumented, '{string.Join(',', missing)}'"); - } + if (missing.Any()) + { + throw new XunitException($"Expected lines to be instrumented, '{string.Join(',', missing)}'"); + } - return document; - } + return document; + } - private static BuildConfiguration GetAssemblyBuildConfiguration() - { + private static BuildConfiguration GetAssemblyBuildConfiguration() + { #if DEBUG - return BuildConfiguration.Debug; + return BuildConfiguration.Debug; #endif #if RELEASE return BuildConfiguration.Release; #endif - throw new NotSupportedException($"Build configuration not supported"); - } + throw new NotSupportedException($"Build configuration not supported"); } + } } diff --git a/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs b/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs index c2a218592..f04233430 100644 --- a/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs +++ b/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs @@ -21,316 +21,316 @@ namespace Coverlet.Core.Tests { - static class TestInstrumentationHelper + static class TestInstrumentationHelper + { + private static IServiceProvider s_processWideContainer; + + /// + /// caller sample: TestInstrumentationHelper.GenerateHtmlReport(result, sourceFileFilter: @"+**\Samples\Instrumentation.cs"); + /// TestInstrumentationHelper.GenerateHtmlReport(result); + /// + public static void GenerateHtmlReport(CoverageResult coverageResult, IReporter reporter = null, string sourceFileFilter = "", [CallerMemberName] string directory = "") { - private static IServiceProvider s_processWideContainer; - - /// - /// caller sample: TestInstrumentationHelper.GenerateHtmlReport(result, sourceFileFilter: @"+**\Samples\Instrumentation.cs"); - /// TestInstrumentationHelper.GenerateHtmlReport(result); - /// - public static void GenerateHtmlReport(CoverageResult coverageResult, IReporter reporter = null, string sourceFileFilter = "", [CallerMemberName] string directory = "") - { - var defaultReporter = new JsonReporter(); - reporter ??= new CoberturaReporter(); - DirectoryInfo dir = Directory.CreateDirectory(directory); - dir.Delete(true); - dir.Create(); - string reportFile = Path.Combine(dir.FullName, Path.ChangeExtension("report", defaultReporter.Extension)); - File.WriteAllText(reportFile, defaultReporter.Report(coverageResult, new Mock().Object)); - reportFile = Path.Combine(dir.FullName, Path.ChangeExtension("report", reporter.Extension)); - File.WriteAllText(reportFile, reporter.Report(coverageResult, new Mock().Object)); - // i.e. reportgenerator -reports:"C:\git\coverlet\test\coverlet.core.tests\bin\Debug\netcoreapp2.0\Condition_If\report.cobertura.xml" -targetdir:"C:\git\coverlet\test\coverlet.core.tests\bin\Debug\netcoreapp2.0\Condition_If" -filefilters:+**\Samples\Instrumentation.cs - Assert.True(new Generator().GenerateReport(new ReportConfiguration( - new[] { reportFile }, - dir.FullName, - new string[0], - null, - new string[0], - new string[0], - new string[0], - new string[0], - string.IsNullOrEmpty(sourceFileFilter) ? new string[0] : new[] { sourceFileFilter }, - null, - null))); - } - - public static CoverageResult GetCoverageResult(string filePath) - { - SetTestContainer(); - using var result = new FileStream(filePath, FileMode.Open); - var logger = new Mock(); - logger.Setup(l => l.LogVerbose(It.IsAny())).Callback((string message) => - { - Assert.DoesNotContain("not found for module: ", message); - }); - s_processWideContainer.GetRequiredService().SetLogger(logger.Object); - var coveragePrepareResultLoaded = CoveragePrepareResult.Deserialize(result); - var coverage = new Coverage(coveragePrepareResultLoaded, logger.Object, s_processWideContainer.GetService(), new FileSystem(), new SourceRootTranslator(new Mock().Object, new FileSystem())); - return coverage.GetCoverageResult(); - } - - public static async Task Run(Func callMethod, - Func includeFilter = null, - Func excludeFilter = null, - Func doesNotReturnAttributes = null, - string persistPrepareResultToFile = null, - bool disableRestoreModules = false, - bool skipAutoProps = false, - string assemblyLocation = null) - { - if (persistPrepareResultToFile is null) - { - throw new ArgumentNullException(nameof(persistPrepareResultToFile)); - } - - // Rename test file to avoid locks - string location = typeof(T).Assembly.Location; - string fileName = Path.ChangeExtension($"testgen_{Path.GetFileNameWithoutExtension(Path.GetRandomFileName())}", ".dll"); - string logFile = Path.ChangeExtension(fileName, ".log"); - string newPath = Path.Combine(Path.GetDirectoryName(location), fileName); - - File.Copy(location, newPath); - File.Copy(Path.ChangeExtension(location, ".pdb"), Path.ChangeExtension(newPath, ".pdb")); + var defaultReporter = new JsonReporter(); + reporter ??= new CoberturaReporter(); + DirectoryInfo dir = Directory.CreateDirectory(directory); + dir.Delete(true); + dir.Create(); + string reportFile = Path.Combine(dir.FullName, Path.ChangeExtension("report", defaultReporter.Extension)); + File.WriteAllText(reportFile, defaultReporter.Report(coverageResult, new Mock().Object)); + reportFile = Path.Combine(dir.FullName, Path.ChangeExtension("report", reporter.Extension)); + File.WriteAllText(reportFile, reporter.Report(coverageResult, new Mock().Object)); + // i.e. reportgenerator -reports:"C:\git\coverlet\test\coverlet.core.tests\bin\Debug\netcoreapp2.0\Condition_If\report.cobertura.xml" -targetdir:"C:\git\coverlet\test\coverlet.core.tests\bin\Debug\netcoreapp2.0\Condition_If" -filefilters:+**\Samples\Instrumentation.cs + Assert.True(new Generator().GenerateReport(new ReportConfiguration( + new[] { reportFile }, + dir.FullName, + new string[0], + null, + new string[0], + new string[0], + new string[0], + new string[0], + string.IsNullOrEmpty(sourceFileFilter) ? new string[0] : new[] { sourceFileFilter }, + null, + null))); + } - string sourceRootTranslatorModulePath = assemblyLocation ?? newPath; - SetTestContainer(sourceRootTranslatorModulePath, disableRestoreModules); - - static string[] defaultFilters(string _) => Array.Empty(); + public static CoverageResult GetCoverageResult(string filePath) + { + SetTestContainer(); + using var result = new FileStream(filePath, FileMode.Open); + var logger = new Mock(); + logger.Setup(l => l.LogVerbose(It.IsAny())).Callback((string message) => + { + Assert.DoesNotContain("not found for module: ", message); + }); + s_processWideContainer.GetRequiredService().SetLogger(logger.Object); + var coveragePrepareResultLoaded = CoveragePrepareResult.Deserialize(result); + var coverage = new Coverage(coveragePrepareResultLoaded, logger.Object, s_processWideContainer.GetService(), new FileSystem(), new SourceRootTranslator(new Mock().Object, new FileSystem())); + return coverage.GetCoverageResult(); + } - var parameters = new CoverageParameters - { - IncludeFilters = (includeFilter is null ? defaultFilters(fileName) : includeFilter(fileName)).Concat( - new string[] - { + public static async Task Run(Func callMethod, + Func includeFilter = null, + Func excludeFilter = null, + Func doesNotReturnAttributes = null, + string persistPrepareResultToFile = null, + bool disableRestoreModules = false, + bool skipAutoProps = false, + string assemblyLocation = null) + { + if (persistPrepareResultToFile is null) + { + throw new ArgumentNullException(nameof(persistPrepareResultToFile)); + } + + // Rename test file to avoid locks + string location = typeof(T).Assembly.Location; + string fileName = Path.ChangeExtension($"testgen_{Path.GetFileNameWithoutExtension(Path.GetRandomFileName())}", ".dll"); + string logFile = Path.ChangeExtension(fileName, ".log"); + string newPath = Path.Combine(Path.GetDirectoryName(location), fileName); + + File.Copy(location, newPath); + File.Copy(Path.ChangeExtension(location, ".pdb"), Path.ChangeExtension(newPath, ".pdb")); + + string sourceRootTranslatorModulePath = assemblyLocation ?? newPath; + SetTestContainer(sourceRootTranslatorModulePath, disableRestoreModules); + + static string[] defaultFilters(string _) => Array.Empty(); + + var parameters = new CoverageParameters + { + IncludeFilters = (includeFilter is null ? defaultFilters(fileName) : includeFilter(fileName)).Concat( + new string[] + { $"[{Path.GetFileNameWithoutExtension(fileName)}*]{GetTypeFullName()}*" - }).ToArray(), - IncludeDirectories = Array.Empty(), - ExcludeFilters = (excludeFilter is null ? defaultFilters(fileName) : excludeFilter(fileName)).Concat(new string[] - { + }).ToArray(), + IncludeDirectories = Array.Empty(), + ExcludeFilters = (excludeFilter is null ? defaultFilters(fileName) : excludeFilter(fileName)).Concat(new string[] + { "[xunit.*]*", "[coverlet.*]*" - }).ToArray(), - ExcludedSourceFiles = Array.Empty(), - ExcludeAttributes = Array.Empty(), - IncludeTestAssembly = true, - SingleHit = false, - MergeWith = string.Empty, - UseSourceLink = false, - SkipAutoProps = skipAutoProps, - DoesNotReturnAttributes = doesNotReturnAttributes?.Invoke(fileName) - }; - - // Instrument module - var coverage = new Coverage(newPath, parameters, new Logger(logFile), - s_processWideContainer.GetService(), s_processWideContainer.GetService(), s_processWideContainer.GetService(), s_processWideContainer.GetService()); - CoveragePrepareResult prepareResult = coverage.PrepareModules(); - - Assert.Single(prepareResult.Results); - - // Load new assembly - var asm = Assembly.LoadFile(newPath); - - // Instance type and call method - await callMethod(Activator.CreateInstance(asm.GetType(typeof(T).FullName))); - - // Flush tracker - Type tracker = asm.GetTypes().Single(n => n.FullName.Contains("Coverlet.Core.Instrumentation.Tracker")); - - // For debugging purpouse - // int[] hitsArray = (int[])tracker.GetField("HitsArray").GetValue(null); - // string hitsFilePath = (string)tracker.GetField("HitsFilePath").GetValue(null); - - // Void UnloadModule(System.Object, System.EventArgs) - tracker.GetTypeInfo().GetMethod("UnloadModule").Invoke(null, new object[2] { null, null }); - - // Persist CoveragePrepareResult - using (var fs = new FileStream(persistPrepareResultToFile, FileMode.Open)) - { - await CoveragePrepareResult.Serialize(prepareResult).CopyToAsync(fs); - } - - return prepareResult; - } + }).ToArray(), + ExcludedSourceFiles = Array.Empty(), + ExcludeAttributes = Array.Empty(), + IncludeTestAssembly = true, + SingleHit = false, + MergeWith = string.Empty, + UseSourceLink = false, + SkipAutoProps = skipAutoProps, + DoesNotReturnAttributes = doesNotReturnAttributes?.Invoke(fileName) + }; + + // Instrument module + var coverage = new Coverage(newPath, parameters, new Logger(logFile), + s_processWideContainer.GetService(), s_processWideContainer.GetService(), s_processWideContainer.GetService(), s_processWideContainer.GetService()); + CoveragePrepareResult prepareResult = coverage.PrepareModules(); + + Assert.Single(prepareResult.Results); + + // Load new assembly + var asm = Assembly.LoadFile(newPath); + + // Instance type and call method + await callMethod(Activator.CreateInstance(asm.GetType(typeof(T).FullName))); + + // Flush tracker + Type tracker = asm.GetTypes().Single(n => n.FullName.Contains("Coverlet.Core.Instrumentation.Tracker")); + + // For debugging purpouse + // int[] hitsArray = (int[])tracker.GetField("HitsArray").GetValue(null); + // string hitsFilePath = (string)tracker.GetField("HitsFilePath").GetValue(null); + + // Void UnloadModule(System.Object, System.EventArgs) + tracker.GetTypeInfo().GetMethod("UnloadModule").Invoke(null, new object[2] { null, null }); + + // Persist CoveragePrepareResult + using (var fs = new FileStream(persistPrepareResultToFile, FileMode.Open)) + { + await CoveragePrepareResult.Serialize(prepareResult).CopyToAsync(fs); + } + + return prepareResult; + } - private static void SetTestContainer(string testModule = null, bool disableRestoreModules = false) + private static void SetTestContainer(string testModule = null, bool disableRestoreModules = false) + { + LazyInitializer.EnsureInitialized(ref s_processWideContainer, () => + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(_ => new Mock().Object); + + // We need to keep singleton/static semantics + if (disableRestoreModules) { - LazyInitializer.EnsureInitialized(ref s_processWideContainer, () => - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(_ => new Mock().Object); - - // We need to keep singleton/static semantics - if (disableRestoreModules) - { - serviceCollection.AddSingleton(); - } - else - { - serviceCollection.AddSingleton(); - } - serviceCollection.AddSingleton(serviceProvider => - string.IsNullOrEmpty(testModule) ? - new SourceRootTranslator(serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()) : - new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService())); - - serviceCollection.AddSingleton(); - - return serviceCollection.BuildServiceProvider(); - }); + serviceCollection.AddSingleton(); } - - private static string GetTypeFullName() + else { - string name = typeof(T).FullName; - if (typeof(T).IsGenericType && name != null) - { - int index = name.IndexOf('`'); - return index == -1 ? name : name[..index]; - } - return name; + serviceCollection.AddSingleton(); } + serviceCollection.AddSingleton(serviceProvider => + string.IsNullOrEmpty(testModule) ? + new SourceRootTranslator(serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()) : + new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService())); + + serviceCollection.AddSingleton(); + + return serviceCollection.BuildServiceProvider(); + }); } - class CustomProcessExitHandler : IProcessExitHandler + private static string GetTypeFullName() { - public void Add(EventHandler handler) - { - // We don't subscribe to process exit, we let parent restore module. - // On msbuild/console/collector code run inside same app domain so statics list of - // files to restore are shared, but on test we run instrumentation on child process - // so there is a race between parent/child on files restore. - // In normal condition Process.Exit try to restore files only in case of - // exception and if in InstrumentationHelper._backupList there are files remained. - } + string name = typeof(T).FullName; + if (typeof(T).IsGenericType && name != null) + { + int index = name.IndexOf('`'); + return index == -1 ? name : name[..index]; + } + return name; + } + } + + class CustomProcessExitHandler : IProcessExitHandler + { + public void Add(EventHandler handler) + { + // We don't subscribe to process exit, we let parent restore module. + // On msbuild/console/collector code run inside same app domain so statics list of + // files to restore are shared, but on test we run instrumentation on child process + // so there is a race between parent/child on files restore. + // In normal condition Process.Exit try to restore files only in case of + // exception and if in InstrumentationHelper._backupList there are files remained. } + } - class CustomRetryHelper : IRetryHelper + class CustomRetryHelper : IRetryHelper + { + public T Do(Func action, Func backoffStrategy, int maxAttemptCount = 3) { - public T Do(Func action, Func backoffStrategy, int maxAttemptCount = 3) + var exceptions = new List(); + for (int attempted = 0; attempted < maxAttemptCount; attempted++) + { + try { - var exceptions = new List(); - for (int attempted = 0; attempted < maxAttemptCount; attempted++) - { - try - { - if (attempted > 0) - { - Thread.Sleep(backoffStrategy()); - } - return action(); - } - catch (Exception ex) - { - if (ex.ToString().Contains("RestoreOriginalModules") || ex.ToString().Contains("RestoreOriginalModule")) - { - // If we're restoring modules mean that process are closing and we cannot override copied test file because is locked so we hide error - // to have a correct process exit value - return default; - } - else - { - exceptions.Add(ex); - } - } - } - throw new AggregateException(exceptions); + if (attempted > 0) + { + Thread.Sleep(backoffStrategy()); + } + return action(); } - - public void Retry(Action action, Func backoffStrategy, int maxAttemptCount = 3) + catch (Exception ex) { - Do(() => - { - action(); - return null; - }, backoffStrategy, maxAttemptCount); + if (ex.ToString().Contains("RestoreOriginalModules") || ex.ToString().Contains("RestoreOriginalModule")) + { + // If we're restoring modules mean that process are closing and we cannot override copied test file because is locked so we hide error + // to have a correct process exit value + return default; + } + else + { + exceptions.Add(ex); + } } + } + throw new AggregateException(exceptions); } - // We log to files for debugging pourpose, we can check if instrumentation is ok - class Logger : ILogger + public void Retry(Action action, Func backoffStrategy, int maxAttemptCount = 3) { - readonly string _logFile; + Do(() => + { + action(); + return null; + }, backoffStrategy, maxAttemptCount); + } + } - public Logger(string logFile) => _logFile = logFile; + // We log to files for debugging pourpose, we can check if instrumentation is ok + class Logger : ILogger + { + readonly string _logFile; - public void LogError(string message) - { - File.AppendAllText(_logFile, message + Environment.NewLine); - } + public Logger(string logFile) => _logFile = logFile; - public void LogError(Exception exception) - { - File.AppendAllText(_logFile, exception.ToString() + Environment.NewLine); - } + public void LogError(string message) + { + File.AppendAllText(_logFile, message + Environment.NewLine); + } - public void LogInformation(string message, bool important = false) - { - File.AppendAllText(_logFile, message + Environment.NewLine); - } + public void LogError(Exception exception) + { + File.AppendAllText(_logFile, exception.ToString() + Environment.NewLine); + } - public void LogVerbose(string message) - { - File.AppendAllText(_logFile, message + Environment.NewLine); - } + public void LogInformation(string message, bool important = false) + { + File.AppendAllText(_logFile, message + Environment.NewLine); + } - public void LogWarning(string message) - { - File.AppendAllText(_logFile, message + Environment.NewLine); - } + public void LogVerbose(string message) + { + File.AppendAllText(_logFile, message + Environment.NewLine); } - class InstrumentationHelperForDebugging : InstrumentationHelper + public void LogWarning(string message) { - public InstrumentationHelperForDebugging(IProcessExitHandler processExitHandler, IRetryHelper retryHelper, IFileSystem fileSystem, ILogger logger, ISourceRootTranslator sourceTranslator) - : base(processExitHandler, retryHelper, fileSystem, logger, sourceTranslator) - { + File.AppendAllText(_logFile, message + Environment.NewLine); + } + } - } + class InstrumentationHelperForDebugging : InstrumentationHelper + { + public InstrumentationHelperForDebugging(IProcessExitHandler processExitHandler, IRetryHelper retryHelper, IFileSystem fileSystem, ILogger logger, ISourceRootTranslator sourceTranslator) + : base(processExitHandler, retryHelper, fileSystem, logger, sourceTranslator) + { - public override void RestoreOriginalModule(string module, string identifier) - { - // DO NOT RESTORE - } + } - public override void RestoreOriginalModules() - { - // DO NOT RESTORE - } + public override void RestoreOriginalModule(string module, string identifier) + { + // DO NOT RESTORE } - public abstract class ExternalProcessExecutionTest + public override void RestoreOriginalModules() { - protected FunctionExecutor FunctionExecutor = new( - o => - { - o.StartInfo.RedirectStandardError = true; - o.OnExit = p => - { - if (p.ExitCode != 0) - { - string message = $"Function exit code failed with exit code: {p.ExitCode}" + Environment.NewLine + - p.StandardError.ReadToEnd(); - throw new Xunit.Sdk.XunitException(message); - } - }; - }); + // DO NOT RESTORE } + } - public static class FunctionExecutorExtensions + public abstract class ExternalProcessExecutionTest + { + protected FunctionExecutor FunctionExecutor = new( + o => { - public static void RunInProcess(this FunctionExecutor executor, Func> func, string[] args) - { - Assert.Equal(0, func(args).Result); - } + o.StartInfo.RedirectStandardError = true; + o.OnExit = p => + { + if (p.ExitCode != 0) + { + string message = $"Function exit code failed with exit code: {p.ExitCode}" + Environment.NewLine + + p.StandardError.ReadToEnd(); + throw new Xunit.Sdk.XunitException(message); + } + }; + }); + } - public static void RunInProcess(this FunctionExecutor executor, Func> func) - { - Assert.Equal(0, func().Result); - } + public static class FunctionExecutorExtensions + { + public static void RunInProcess(this FunctionExecutor executor, Func> func, string[] args) + { + Assert.Equal(0, func(args).Result); + } + + public static void RunInProcess(this FunctionExecutor executor, Func> func) + { + Assert.Equal(0, func().Result); } + } } diff --git a/test/coverlet.core.tests/CoverageResultTests.cs b/test/coverlet.core.tests/CoverageResultTests.cs index fd5450909..4d7ef7db8 100644 --- a/test/coverlet.core.tests/CoverageResultTests.cs +++ b/test/coverlet.core.tests/CoverageResultTests.cs @@ -7,170 +7,170 @@ namespace Coverlet.Core.Tests { - public class CoverageResultTests + public class CoverageResultTests + { + private readonly Modules _modules; + + public CoverageResultTests() { - private readonly Modules _modules; - - public CoverageResultTests() - { - var lines = new Lines(); - lines.Add(1, 1); - lines.Add(2, 1); - lines.Add(3, 1); - var branches = new Branches(); - branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 0, Ordinal = 1 }); - branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 1, Ordinal = 2 }); - branches.Add(new BranchInfo { Line = 2, Hits = 0, Offset = 1, Path = 0, Ordinal = 1 }); - - // System.Void Coverlet.Core.Tests.CoverageResultTests::CoverageResultTests - 3/3 100% line 2/3 66.7% branch coverage - var methods = new Methods(); - string methodString = "System.Void Coverlet.Core.Tests.CoverageResultTests::CoverageResultTests()"; - methods.Add(methodString, new Method()); - methods[methodString].Lines = lines; - methods[methodString].Branches = branches; - - // System.Void Coverlet.Core.Tests.CoverageResultTests::GetThresholdTypesBelowThreshold - 0/2 0% line - methodString = "System.Void Coverlet.Core.Tests.CoverageResultTests::GetThresholdTypesBelowThreshold()"; - methods.Add(methodString, new Method()); - methods[methodString].Lines = new Lines() + var lines = new Lines(); + lines.Add(1, 1); + lines.Add(2, 1); + lines.Add(3, 1); + var branches = new Branches(); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 0, Ordinal = 1 }); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 1, Path = 1, Ordinal = 2 }); + branches.Add(new BranchInfo { Line = 2, Hits = 0, Offset = 1, Path = 0, Ordinal = 1 }); + + // System.Void Coverlet.Core.Tests.CoverageResultTests::CoverageResultTests - 3/3 100% line 2/3 66.7% branch coverage + var methods = new Methods(); + string methodString = "System.Void Coverlet.Core.Tests.CoverageResultTests::CoverageResultTests()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; + + // System.Void Coverlet.Core.Tests.CoverageResultTests::GetThresholdTypesBelowThreshold - 0/2 0% line + methodString = "System.Void Coverlet.Core.Tests.CoverageResultTests::GetThresholdTypesBelowThreshold()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = new Lines() { {1, 0}, {2, 0}, }; - var classes = new Classes(); - classes.Add("Coverlet.Core.Tests.CoverageResultTests", methods); - // Methods - 1/2 (50%) - // Lines - 3/5 (60%) - // Branches - 2/3 (66.67%) + var classes = new Classes(); + classes.Add("Coverlet.Core.Tests.CoverageResultTests", methods); + // Methods - 1/2 (50%) + // Lines - 3/5 (60%) + // Branches - 2/3 (66.67%) - var documents = new Documents(); - documents.Add("doc.cs", classes); + var documents = new Documents(); + documents.Add("doc.cs", classes); - _modules = new Modules(); - _modules.Add("module", documents); - } + _modules = new Modules(); + _modules.Add("module", documents); + } - [Fact] - public void TestGetThresholdTypesBelowThresholdLine() - { - var result = new CoverageResult(); - result.Modules = _modules; + [Fact] + public void TestGetThresholdTypesBelowThresholdLine() + { + var result = new CoverageResult(); + result.Modules = _modules; - var summary = new CoverageSummary(); - var thresholdTypeFlagValues = new Dictionary() + var summary = new CoverageSummary(); + var thresholdTypeFlagValues = new Dictionary() { { ThresholdTypeFlags.Line, 90 }, { ThresholdTypeFlags.Method, 10 }, { ThresholdTypeFlags.Branch, 10 }, }; - ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; - ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); - Assert.Equal(ThresholdTypeFlags.Line, resThresholdTypeFlags); - } + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); + Assert.Equal(ThresholdTypeFlags.Line, resThresholdTypeFlags); + } - [Fact] - public void TestGetThresholdTypesBelowThresholdMethod() - { - var result = new CoverageResult(); - result.Modules = _modules; + [Fact] + public void TestGetThresholdTypesBelowThresholdMethod() + { + var result = new CoverageResult(); + result.Modules = _modules; - var summary = new CoverageSummary(); - var thresholdTypeFlagValues = new Dictionary() + var summary = new CoverageSummary(); + var thresholdTypeFlagValues = new Dictionary() { { ThresholdTypeFlags.Line, 50 }, { ThresholdTypeFlags.Method, 75 }, { ThresholdTypeFlags.Branch, 10 }, }; - ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; - ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); - Assert.Equal(ThresholdTypeFlags.Method, resThresholdTypeFlags); - } + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); + Assert.Equal(ThresholdTypeFlags.Method, resThresholdTypeFlags); + } - [Fact] - public void TestGetThresholdTypesBelowThresholdBranch() - { - var result = new CoverageResult(); - result.Modules = _modules; + [Fact] + public void TestGetThresholdTypesBelowThresholdBranch() + { + var result = new CoverageResult(); + result.Modules = _modules; - var summary = new CoverageSummary(); - var thresholdTypeFlagValues = new Dictionary() + var summary = new CoverageSummary(); + var thresholdTypeFlagValues = new Dictionary() { { ThresholdTypeFlags.Line, 50 }, { ThresholdTypeFlags.Method, 50 }, { ThresholdTypeFlags.Branch, 90 }, }; - ThresholdStatistic thresholdStatic = ThresholdStatistic.Total; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Total; - ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); - Assert.Equal(ThresholdTypeFlags.Branch, resThresholdTypeFlags); - } + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); + Assert.Equal(ThresholdTypeFlags.Branch, resThresholdTypeFlags); + } - [Fact] - public void TestGetThresholdTypesBelowThresholdAllGood() - { - var result = new CoverageResult(); - result.Modules = _modules; + [Fact] + public void TestGetThresholdTypesBelowThresholdAllGood() + { + var result = new CoverageResult(); + result.Modules = _modules; - var summary = new CoverageSummary(); - var thresholdTypeFlagValues = new Dictionary() + var summary = new CoverageSummary(); + var thresholdTypeFlagValues = new Dictionary() { { ThresholdTypeFlags.Line, 50 }, { ThresholdTypeFlags.Method, 50 }, { ThresholdTypeFlags.Branch, 50 }, }; - ThresholdStatistic thresholdStatic = ThresholdStatistic.Average; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Average; - ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); - Assert.Equal(ThresholdTypeFlags.None, resThresholdTypeFlags); - } + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); + Assert.Equal(ThresholdTypeFlags.None, resThresholdTypeFlags); + } - [Fact] - public void TestGetThresholdTypesBelowThresholdAllFail() - { - var result = new CoverageResult(); - result.Modules = _modules; + [Fact] + public void TestGetThresholdTypesBelowThresholdAllFail() + { + var result = new CoverageResult(); + result.Modules = _modules; - var summary = new CoverageSummary(); - var thresholdTypeFlagValues = new Dictionary() + var summary = new CoverageSummary(); + var thresholdTypeFlagValues = new Dictionary() { { ThresholdTypeFlags.Line, 100 }, { ThresholdTypeFlags.Method, 100 }, { ThresholdTypeFlags.Branch, 100 }, }; - ThresholdTypeFlags thresholdTypeFlags = ThresholdTypeFlags.Line | ThresholdTypeFlags.Branch | ThresholdTypeFlags.Method; - ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; + ThresholdTypeFlags thresholdTypeFlags = ThresholdTypeFlags.Line | ThresholdTypeFlags.Branch | ThresholdTypeFlags.Method; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; - ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); - Assert.Equal(thresholdTypeFlags, resThresholdTypeFlags); - } + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); + Assert.Equal(thresholdTypeFlags, resThresholdTypeFlags); + } - [Fact] - public void TestGetThresholdTypesBelowThresholdWhenNoModuleInstrumented() - { - var result = new CoverageResult(); - result.Modules = new Modules(); + [Fact] + public void TestGetThresholdTypesBelowThresholdWhenNoModuleInstrumented() + { + var result = new CoverageResult(); + result.Modules = new Modules(); - var summary = new CoverageSummary(); - var thresholdTypeFlagValues = new Dictionary() + var summary = new CoverageSummary(); + var thresholdTypeFlagValues = new Dictionary() { { ThresholdTypeFlags.Line, 80 }, { ThresholdTypeFlags.Method, 80 }, { ThresholdTypeFlags.Branch, 80 }, }; - ThresholdTypeFlags thresholdTypeFlags = ThresholdTypeFlags.Line | ThresholdTypeFlags.Branch | ThresholdTypeFlags.Method; - ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; + ThresholdTypeFlags thresholdTypeFlags = ThresholdTypeFlags.Line | ThresholdTypeFlags.Branch | ThresholdTypeFlags.Method; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; - ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); - Assert.Equal(thresholdTypeFlags, resThresholdTypeFlags); - } + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); + Assert.Equal(thresholdTypeFlags, resThresholdTypeFlags); } + } } diff --git a/test/coverlet.core.tests/Helpers/FileSystemTests.cs b/test/coverlet.core.tests/Helpers/FileSystemTests.cs index 10f4aa289..897891e8b 100644 --- a/test/coverlet.core.tests/Helpers/FileSystemTests.cs +++ b/test/coverlet.core.tests/Helpers/FileSystemTests.cs @@ -1,22 +1,22 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Xunit; namespace Coverlet.Core.Helpers.Tests { - public class FileSystemTests + public class FileSystemTests + { + [Theory] + [InlineData(null, null)] + [InlineData("", "")] + [InlineData("filename.cs", "filename.cs")] + [InlineData("filename{T}.cs", "filename{{T}}.cs")] + public void TestEscapeFileName(string fileName, string expected) { - [Theory] - [InlineData(null, null)] - [InlineData("", "")] - [InlineData("filename.cs", "filename.cs")] - [InlineData("filename{T}.cs", "filename{{T}}.cs")] - public void TestEscapeFileName(string fileName, string expected) - { - string actual = FileSystem.EscapeFileName(fileName); + string actual = FileSystem.EscapeFileName(fileName); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); } -} \ No newline at end of file + } +} diff --git a/test/coverlet.core.tests/Helpers/RetryHelperTests.cs b/test/coverlet.core.tests/Helpers/RetryHelperTests.cs index acbcbd41f..dd7232c80 100644 --- a/test/coverlet.core.tests/Helpers/RetryHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/RetryHelperTests.cs @@ -6,77 +6,77 @@ namespace Coverlet.Core.Helpers.Tests { - public class RetryHelperTests + public class RetryHelperTests + { + [Fact] + public void TestRetryWithFixedRetryBackoff() { - [Fact] - public void TestRetryWithFixedRetryBackoff() - { - Func retryStrategy = () => - { - return TimeSpan.FromMilliseconds(1); - }; + Func retryStrategy = () => + { + return TimeSpan.FromMilliseconds(1); + }; - var target = new RetryTarget(); - try - { - new RetryHelper().Retry(() => target.TargetActionThrows(), retryStrategy, 7); - } - catch - { - Assert.Equal(7, target.Calls); - } - } - - [Fact] - public void TestRetryWithExponentialRetryBackoff() - { - int currentSleep = 6; - Func retryStrategy = () => - { - var sleep = TimeSpan.FromMilliseconds(currentSleep); - currentSleep *= 2; - return sleep; - }; + var target = new RetryTarget(); + try + { + new RetryHelper().Retry(() => target.TargetActionThrows(), retryStrategy, 7); + } + catch + { + Assert.Equal(7, target.Calls); + } + } - var target = new RetryTarget(); - try - { - new RetryHelper().Retry(() => target.TargetActionThrows(), retryStrategy, 3); - } - catch - { - Assert.Equal(3, target.Calls); - Assert.Equal(24, currentSleep); - } - } + [Fact] + public void TestRetryWithExponentialRetryBackoff() + { + int currentSleep = 6; + Func retryStrategy = () => + { + var sleep = TimeSpan.FromMilliseconds(currentSleep); + currentSleep *= 2; + return sleep; + }; - [Fact] - public void TestRetryFinishesIfSuccessful() - { - Func retryStrategy = () => - { - return TimeSpan.FromMilliseconds(1); - }; + var target = new RetryTarget(); + try + { + new RetryHelper().Retry(() => target.TargetActionThrows(), retryStrategy, 3); + } + catch + { + Assert.Equal(3, target.Calls); + Assert.Equal(24, currentSleep); + } + } - var target = new RetryTarget(); - new RetryHelper().Retry(() => target.TargetActionThrows5Times(), retryStrategy, 20); - Assert.Equal(6, target.Calls); - } + [Fact] + public void TestRetryFinishesIfSuccessful() + { + Func retryStrategy = () => + { + return TimeSpan.FromMilliseconds(1); + }; + var target = new RetryTarget(); + new RetryHelper().Retry(() => target.TargetActionThrows5Times(), retryStrategy, 20); + Assert.Equal(6, target.Calls); } - public class RetryTarget + } + + public class RetryTarget + { + public int Calls { get; set; } + public void TargetActionThrows() + { + Calls++; + throw new Exception("Simulating Failure"); + } + public void TargetActionThrows5Times() { - public int Calls { get; set; } - public void TargetActionThrows() - { - Calls++; - throw new Exception("Simulating Failure"); - } - public void TargetActionThrows5Times() - { - Calls++; - if (Calls < 6) throw new Exception("Simulating Failure"); - } + Calls++; + if (Calls < 6) throw new Exception("Simulating Failure"); } + } } diff --git a/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs b/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs index e464635a3..c09a988f5 100644 --- a/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs +++ b/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs @@ -9,83 +9,83 @@ namespace Coverlet.Core.Helpers.Tests { - public class SourceRootTranslatorTests + public class SourceRootTranslatorTests + { + [ConditionalFact] + [SkipOnOS(OS.Linux, "Windows path format only")] + [SkipOnOS(OS.MacOS, "Windows path format only")] + public void Translate_Success() { - [ConditionalFact] - [SkipOnOS(OS.Linux, "Windows path format only")] - [SkipOnOS(OS.MacOS, "Windows path format only")] - public void Translate_Success() - { - string fileToTranslate = "/_/src/coverlet.core/obj/Debug/netstandard2.0/coverlet.core.pdb"; - var logger = new Mock(); - var assemblyAdapter = new Mock(); - assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("testLib"); - var fileSystem = new Mock(); - fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => - { - if (p == "testLib.dll" || p == @"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb" || p == "CoverletSourceRootsMapping_testLib") return true; - return false; - }); - fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(File.ReadAllLines(@"TestAssets/CoverletSourceRootsMappingTest")); - var translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object, assemblyAdapter.Object); - Assert.Equal(@"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb", translator.ResolveFilePath(fileToTranslate)); - Assert.Equal(@"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb", translator.ResolveFilePath(fileToTranslate)); - } + string fileToTranslate = "/_/src/coverlet.core/obj/Debug/netstandard2.0/coverlet.core.pdb"; + var logger = new Mock(); + var assemblyAdapter = new Mock(); + assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("testLib"); + var fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => + { + if (p == "testLib.dll" || p == @"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb" || p == "CoverletSourceRootsMapping_testLib") return true; + return false; + }); + fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(File.ReadAllLines(@"TestAssets/CoverletSourceRootsMappingTest")); + var translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object, assemblyAdapter.Object); + Assert.Equal(@"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb", translator.ResolveFilePath(fileToTranslate)); + Assert.Equal(@"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb", translator.ResolveFilePath(fileToTranslate)); + } - [ConditionalFact] - [SkipOnOS(OS.Linux, "Windows path format only")] - [SkipOnOS(OS.MacOS, "Windows path format only")] - public void TranslatePathRoot_Success() - { - var logger = new Mock(); - var assemblyAdapter = new Mock(); - assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("testLib"); - var fileSystem = new Mock(); - fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => - { - if (p == "testLib.dll" || p == @"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb" || p == "CoverletSourceRootsMapping_testLib") return true; - return false; - }); - fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(File.ReadAllLines(@"TestAssets/CoverletSourceRootsMappingTest")); - var translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object, assemblyAdapter.Object); - Assert.Equal(@"C:\git\coverlet\", translator.ResolvePathRoot("/_/")[0].OriginalPath); - } + [ConditionalFact] + [SkipOnOS(OS.Linux, "Windows path format only")] + [SkipOnOS(OS.MacOS, "Windows path format only")] + public void TranslatePathRoot_Success() + { + var logger = new Mock(); + var assemblyAdapter = new Mock(); + assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("testLib"); + var fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => + { + if (p == "testLib.dll" || p == @"C:\git\coverlet\src\coverlet.core\obj\Debug\netstandard2.0\coverlet.core.pdb" || p == "CoverletSourceRootsMapping_testLib") return true; + return false; + }); + fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(File.ReadAllLines(@"TestAssets/CoverletSourceRootsMappingTest")); + var translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object, assemblyAdapter.Object); + Assert.Equal(@"C:\git\coverlet\", translator.ResolvePathRoot("/_/")[0].OriginalPath); + } - [Fact] - public void Translate_EmptyFile() - { - string fileToTranslate = "/_/src/coverlet.core/obj/Debug/netstandard2.0/coverlet.core.pdb"; - var logger = new Mock(); - var assemblyAdapter = new Mock(); - assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("testLib"); - var fileSystem = new Mock(); - fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => - { - if (p == "testLib.dll" || p == "CoverletSourceRootsMapping_testLib") return true; - return false; - }); - fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(new string[0]); - var translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object, assemblyAdapter.Object); - Assert.Equal(fileToTranslate, translator.ResolveFilePath(fileToTranslate)); - } + [Fact] + public void Translate_EmptyFile() + { + string fileToTranslate = "/_/src/coverlet.core/obj/Debug/netstandard2.0/coverlet.core.pdb"; + var logger = new Mock(); + var assemblyAdapter = new Mock(); + assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("testLib"); + var fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => + { + if (p == "testLib.dll" || p == "CoverletSourceRootsMapping_testLib") return true; + return false; + }); + fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(new string[0]); + var translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object, assemblyAdapter.Object); + Assert.Equal(fileToTranslate, translator.ResolveFilePath(fileToTranslate)); + } - [Fact] - public void Translate_MalformedFile() - { - string fileToTranslate = "/_/src/coverlet.core/obj/Debug/netstandard2.0/coverlet.core.pdb"; - var logger = new Mock(); - var assemblyAdapter = new Mock(); - assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("testLib"); - var fileSystem = new Mock(); - fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => - { - if (p == "testLib.dll" || p == "CoverletSourceRootsMapping_testLib") return true; - return false; - }); - fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(new string[1] { "malformedRow" }); - var translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object, assemblyAdapter.Object); - Assert.Equal(fileToTranslate, translator.ResolveFilePath(fileToTranslate)); - logger.Verify(l => l.LogWarning(It.IsAny()), Times.Once); - } + [Fact] + public void Translate_MalformedFile() + { + string fileToTranslate = "/_/src/coverlet.core/obj/Debug/netstandard2.0/coverlet.core.pdb"; + var logger = new Mock(); + var assemblyAdapter = new Mock(); + assemblyAdapter.Setup(x => x.GetAssemblyName(It.IsAny())).Returns("testLib"); + var fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => + { + if (p == "testLib.dll" || p == "CoverletSourceRootsMapping_testLib") return true; + return false; + }); + fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(new string[1] { "malformedRow" }); + var translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object, assemblyAdapter.Object); + Assert.Equal(fileToTranslate, translator.ResolveFilePath(fileToTranslate)); + logger.Verify(l => l.LogWarning(It.IsAny()), Times.Once); } + } } diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterResultTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterResultTests.cs index f3ca69aed..02751ea14 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterResultTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterResultTests.cs @@ -1,162 +1,162 @@ // Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Xunit; using System.Linq; +using Xunit; namespace Coverlet.Core.Instrumentation.Tests { - public class InstrumenterResultTests + public class InstrumenterResultTests + { + [Fact] + public void TestEnsureDocumentsPropertyNotNull() { - [Fact] - public void TestEnsureDocumentsPropertyNotNull() - { - var result = new InstrumenterResult(); - Assert.NotNull(result.Documents); - } + var result = new InstrumenterResult(); + Assert.NotNull(result.Documents); + } + + [Fact] + public void TestEnsureLinesAndBranchesPropertyNotNull() + { + var document = new Document(); + Assert.NotNull(document.Lines); + Assert.NotNull(document.Branches); + } - [Fact] - public void TestEnsureLinesAndBranchesPropertyNotNull() + [Fact] + public void CoveragePrepareResult_SerializationRoundTrip() + { + var cpr = new CoveragePrepareResult(); + cpr.Identifier = "Identifier"; + cpr.MergeWith = "MergeWith"; + cpr.ModuleOrDirectory = "Module"; + cpr.UseSourceLink = true; + + var ir = new InstrumenterResult(); + ir.HitsFilePath = "HitsFilePath"; + ir.Module = "Module"; + ir.ModulePath = "ModulePath"; + ir.SourceLink = "SourceLink"; + + ir.HitCandidates.Add(new HitCandidate(true, 1, 2, 3)); + ir.HitCandidates.Add(new HitCandidate(false, 4, 5, 6)); + + var doc = new Document() + { + Index = 0, + Path = "Path0" + }; + doc.Lines.Add(0, new Line() + { + Class = "Class0", + Hits = 0, + Method = "Method0", + Number = 0 + }); + doc.Branches.Add(new BranchKey(0, 0), + new Branch() + { + Class = "Class0", + EndOffset = 0, + Hits = 0, + Method = "Method", + Number = 0, + Offset = 0, + Ordinal = 0, + Path = 0 + }); + + var doc2 = new Document() + { + Index = 1, + Path = "Path1" + }; + doc2.Lines.Add(1, new Line() + { + Class = "Class1", + Hits = 1, + Method = "Method1", + Number = 1 + }); + doc2.Branches.Add(new BranchKey(1, 1), + new Branch() + { + Class = "Class1", + EndOffset = 1, + Hits = 1, + Method = "Method1", + Number = 1, + Offset = 1, + Ordinal = 1, + Path = 1 + }); + + ir.Documents.Add("key", doc); + ir.Documents.Add("key2", doc2); + cpr.Results = new InstrumenterResult[] { ir }; + + var roundTrip = CoveragePrepareResult.Deserialize(CoveragePrepareResult.Serialize(cpr)); + + Assert.Equal(cpr.Identifier, roundTrip.Identifier); + Assert.Equal(cpr.MergeWith, roundTrip.MergeWith); + Assert.Equal(cpr.ModuleOrDirectory, roundTrip.ModuleOrDirectory); + Assert.Equal(cpr.UseSourceLink, roundTrip.UseSourceLink); + + for (int i = 0; i < cpr.Results.Length; i++) + { + Assert.Equal(cpr.Results[i].HitsFilePath, roundTrip.Results[i].HitsFilePath); + Assert.Equal(cpr.Results[i].Module, roundTrip.Results[i].Module); + Assert.Equal(cpr.Results[i].ModulePath, roundTrip.Results[i].ModulePath); + Assert.Equal(cpr.Results[i].SourceLink, roundTrip.Results[i].SourceLink); + + for (int k = 0; k < cpr.Results[i].HitCandidates.Count; k++) { - var document = new Document(); - Assert.NotNull(document.Lines); - Assert.NotNull(document.Branches); + Assert.Equal(cpr.Results[i].HitCandidates[k].start, roundTrip.Results[i].HitCandidates[k].start); + Assert.Equal(cpr.Results[i].HitCandidates[k].isBranch, roundTrip.Results[i].HitCandidates[k].isBranch); + Assert.Equal(cpr.Results[i].HitCandidates[k].end, roundTrip.Results[i].HitCandidates[k].end); + Assert.Equal(cpr.Results[i].HitCandidates[k].docIndex, roundTrip.Results[i].HitCandidates[k].docIndex); } - [Fact] - public void CoveragePrepareResult_SerializationRoundTrip() + for (int k = 0; k < cpr.Results[i].Documents.Count; k++) { - var cpr = new CoveragePrepareResult(); - cpr.Identifier = "Identifier"; - cpr.MergeWith = "MergeWith"; - cpr.ModuleOrDirectory = "Module"; - cpr.UseSourceLink = true; - - var ir = new InstrumenterResult(); - ir.HitsFilePath = "HitsFilePath"; - ir.Module = "Module"; - ir.ModulePath = "ModulePath"; - ir.SourceLink = "SourceLink"; - - ir.HitCandidates.Add(new HitCandidate(true, 1, 2, 3)); - ir.HitCandidates.Add(new HitCandidate(false, 4, 5, 6)); - - var doc = new Document() - { - Index = 0, - Path = "Path0" - }; - doc.Lines.Add(0, new Line() - { - Class = "Class0", - Hits = 0, - Method = "Method0", - Number = 0 - }); - doc.Branches.Add(new BranchKey(0, 0), - new Branch() - { - Class = "Class0", - EndOffset = 0, - Hits = 0, - Method = "Method", - Number = 0, - Offset = 0, - Ordinal = 0, - Path = 0 - }); - - var doc2 = new Document() + System.Collections.Generic.KeyValuePair[] documents = cpr.Results[i].Documents.ToArray(); + System.Collections.Generic.KeyValuePair[] documentsRoundTrip = roundTrip.Results[i].Documents.ToArray(); + for (int j = 0; j < documents.Length; j++) + { + Assert.Equal(documents[j].Key, documentsRoundTrip[j].Key); + Assert.Equal(documents[j].Value.Index, documentsRoundTrip[j].Value.Index); + Assert.Equal(documents[j].Value.Path, documentsRoundTrip[j].Value.Path); + + for (int v = 0; v < documents[j].Value.Lines.Count; v++) { - Index = 1, - Path = "Path1" - }; - doc2.Lines.Add(1, new Line() - { - Class = "Class1", - Hits = 1, - Method = "Method1", - Number = 1 - }); - doc2.Branches.Add(new BranchKey(1, 1), - new Branch() - { - Class = "Class1", - EndOffset = 1, - Hits = 1, - Method = "Method1", - Number = 1, - Offset = 1, - Ordinal = 1, - Path = 1 - }); - - ir.Documents.Add("key", doc); - ir.Documents.Add("key2", doc2); - cpr.Results = new InstrumenterResult[] { ir }; - - var roundTrip = CoveragePrepareResult.Deserialize(CoveragePrepareResult.Serialize(cpr)); - - Assert.Equal(cpr.Identifier, roundTrip.Identifier); - Assert.Equal(cpr.MergeWith, roundTrip.MergeWith); - Assert.Equal(cpr.ModuleOrDirectory, roundTrip.ModuleOrDirectory); - Assert.Equal(cpr.UseSourceLink, roundTrip.UseSourceLink); - - for (int i = 0; i < cpr.Results.Length; i++) + System.Collections.Generic.KeyValuePair[] lines = documents[j].Value.Lines.ToArray(); + System.Collections.Generic.KeyValuePair[] linesRoundTrip = documentsRoundTrip[j].Value.Lines.ToArray(); + + Assert.Equal(lines[v].Key, linesRoundTrip[v].Key); + Assert.Equal(lines[v].Value.Class, lines[v].Value.Class); + Assert.Equal(lines[v].Value.Hits, lines[v].Value.Hits); + Assert.Equal(lines[v].Value.Method, lines[v].Value.Method); + Assert.Equal(lines[v].Value.Number, lines[v].Value.Number); + } + + for (int v = 0; v < documents[j].Value.Branches.Count; v++) { - Assert.Equal(cpr.Results[i].HitsFilePath, roundTrip.Results[i].HitsFilePath); - Assert.Equal(cpr.Results[i].Module, roundTrip.Results[i].Module); - Assert.Equal(cpr.Results[i].ModulePath, roundTrip.Results[i].ModulePath); - Assert.Equal(cpr.Results[i].SourceLink, roundTrip.Results[i].SourceLink); - - for (int k = 0; k < cpr.Results[i].HitCandidates.Count; k++) - { - Assert.Equal(cpr.Results[i].HitCandidates[k].start, roundTrip.Results[i].HitCandidates[k].start); - Assert.Equal(cpr.Results[i].HitCandidates[k].isBranch, roundTrip.Results[i].HitCandidates[k].isBranch); - Assert.Equal(cpr.Results[i].HitCandidates[k].end, roundTrip.Results[i].HitCandidates[k].end); - Assert.Equal(cpr.Results[i].HitCandidates[k].docIndex, roundTrip.Results[i].HitCandidates[k].docIndex); - } - - for (int k = 0; k < cpr.Results[i].Documents.Count; k++) - { - System.Collections.Generic.KeyValuePair[] documents = cpr.Results[i].Documents.ToArray(); - System.Collections.Generic.KeyValuePair[] documentsRoundTrip = roundTrip.Results[i].Documents.ToArray(); - for (int j = 0; j < documents.Length; j++) - { - Assert.Equal(documents[j].Key, documentsRoundTrip[j].Key); - Assert.Equal(documents[j].Value.Index, documentsRoundTrip[j].Value.Index); - Assert.Equal(documents[j].Value.Path, documentsRoundTrip[j].Value.Path); - - for (int v = 0; v < documents[j].Value.Lines.Count; v++) - { - System.Collections.Generic.KeyValuePair[] lines = documents[j].Value.Lines.ToArray(); - System.Collections.Generic.KeyValuePair[] linesRoundTrip = documentsRoundTrip[j].Value.Lines.ToArray(); - - Assert.Equal(lines[v].Key, linesRoundTrip[v].Key); - Assert.Equal(lines[v].Value.Class, lines[v].Value.Class); - Assert.Equal(lines[v].Value.Hits, lines[v].Value.Hits); - Assert.Equal(lines[v].Value.Method, lines[v].Value.Method); - Assert.Equal(lines[v].Value.Number, lines[v].Value.Number); - } - - for (int v = 0; v < documents[j].Value.Branches.Count; v++) - { - System.Collections.Generic.KeyValuePair[] branches = documents[j].Value.Branches.ToArray(); - System.Collections.Generic.KeyValuePair[] branchesRoundTrip = documentsRoundTrip[j].Value.Branches.ToArray(); - - Assert.Equal(branches[v].Key, branchesRoundTrip[v].Key); - Assert.Equal(branches[v].Value.Class, branchesRoundTrip[v].Value.Class); - Assert.Equal(branches[v].Value.EndOffset, branchesRoundTrip[v].Value.EndOffset); - Assert.Equal(branches[v].Value.Hits, branchesRoundTrip[v].Value.Hits); - Assert.Equal(branches[v].Value.Method, branchesRoundTrip[v].Value.Method); - Assert.Equal(branches[v].Value.Number, branchesRoundTrip[v].Value.Number); - Assert.Equal(branches[v].Value.Offset, branchesRoundTrip[v].Value.Offset); - Assert.Equal(branches[v].Value.Ordinal, branchesRoundTrip[v].Value.Ordinal); - Assert.Equal(branches[v].Value.Path, branchesRoundTrip[v].Value.Path); - } - } - } + System.Collections.Generic.KeyValuePair[] branches = documents[j].Value.Branches.ToArray(); + System.Collections.Generic.KeyValuePair[] branchesRoundTrip = documentsRoundTrip[j].Value.Branches.ToArray(); + + Assert.Equal(branches[v].Key, branchesRoundTrip[v].Key); + Assert.Equal(branches[v].Value.Class, branchesRoundTrip[v].Value.Class); + Assert.Equal(branches[v].Value.EndOffset, branchesRoundTrip[v].Value.EndOffset); + Assert.Equal(branches[v].Value.Hits, branchesRoundTrip[v].Value.Hits); + Assert.Equal(branches[v].Value.Method, branchesRoundTrip[v].Value.Method); + Assert.Equal(branches[v].Value.Number, branchesRoundTrip[v].Value.Number); + Assert.Equal(branches[v].Value.Offset, branchesRoundTrip[v].Value.Offset); + Assert.Equal(branches[v].Value.Ordinal, branchesRoundTrip[v].Value.Ordinal); + Assert.Equal(branches[v].Value.Path, branchesRoundTrip[v].Value.Path); } + } } + } } + } } diff --git a/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs b/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs index 87ce2f3cc..c31fba1a4 100644 --- a/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs +++ b/test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs @@ -1,12 +1,12 @@ // Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Coverlet.Core.Instrumentation; using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using Coverlet.Core.Instrumentation; using Xunit; namespace Coverlet.Core.Tests.Instrumentation diff --git a/test/coverlet.core.tests/Properties/AssemblyInfo.cs b/test/coverlet.core.tests/Properties/AssemblyInfo.cs index 33d7581ec..6eb5c2089 100644 --- a/test/coverlet.core.tests/Properties/AssemblyInfo.cs +++ b/test/coverlet.core.tests/Properties/AssemblyInfo.cs @@ -1,6 +1,6 @@ -// Copyright (c) Toni Solarin-Sodara +// 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.tests.snk")] \ No newline at end of file +[assembly: AssemblyKeyFile("coverlet.core.tests.snk")] diff --git a/test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs b/test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs index f54174b70..d17524e91 100644 --- a/test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs +++ b/test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs @@ -16,194 +16,194 @@ namespace Coverlet.Core.Reporters.Tests { - public class CoberturaReporterTests + public class CoberturaReporterTests + { + [Fact] + public void TestReport() { - [Fact] - public void TestReport() + var result = new CoverageResult(); + result.Identifier = Guid.NewGuid().ToString(); + + var lines = new Lines(); + lines.Add(1, 1); + lines.Add(2, 0); + + var branches = new Branches(); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }); + branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 23, EndOffset = 27, Path = 1, Ordinal = 2 }); + + var methods = new Methods(); + string methodString = "System.Void Coverlet.Core.Reporters.Tests.CoberturaReporterTests::TestReport()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; + + var classes = new Classes(); + classes.Add("Coverlet.Core.Reporters.Tests.CoberturaReporterTests", methods); + + var documents = new Documents(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + documents.Add(@"C:\doc.cs", classes); + } + else + { + documents.Add(@"/doc.cs", classes); + } + + result.Modules = new Modules(); + result.Modules.Add("module", documents); + result.Parameters = new CoverageParameters(); + + CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("it-IT"); + try + { + // Assert conversion behaviour to be sure to be in a Italian culture context + // where decimal char is comma. + Assert.Equal("1,5", (1.5).ToString()); + + var reporter = new CoberturaReporter(); + string report = reporter.Report(result, new Mock().Object); + + Assert.NotEmpty(report); + + var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); + + IEnumerable matchingRateAttributes = doc.Descendants().Attributes().Where(attr => attr.Name.LocalName.EndsWith("-rate")); + IEnumerable rateParentNodeNames = matchingRateAttributes.Select(attr => attr.Parent.Name.LocalName); + Assert.Contains("package", rateParentNodeNames); + Assert.Contains("class", rateParentNodeNames); + Assert.Contains("method", rateParentNodeNames); + Assert.All(matchingRateAttributes.Select(attr => attr.Value), + value => { - var result = new CoverageResult(); - result.Identifier = Guid.NewGuid().ToString(); - - var lines = new Lines(); - lines.Add(1, 1); - lines.Add(2, 0); - - var branches = new Branches(); - branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }); - branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 23, EndOffset = 27, Path = 1, Ordinal = 2 }); - - var methods = new Methods(); - string methodString = "System.Void Coverlet.Core.Reporters.Tests.CoberturaReporterTests::TestReport()"; - methods.Add(methodString, new Method()); - methods[methodString].Lines = lines; - methods[methodString].Branches = branches; - - var classes = new Classes(); - classes.Add("Coverlet.Core.Reporters.Tests.CoberturaReporterTests", methods); - - var documents = new Documents(); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - documents.Add(@"C:\doc.cs", classes); - } - else - { - documents.Add(@"/doc.cs", classes); - } - - result.Modules = new Modules(); - result.Modules.Add("module", documents); - result.Parameters = new CoverageParameters(); - - CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture; - Thread.CurrentThread.CurrentCulture = new CultureInfo("it-IT"); - try - { - // Assert conversion behaviour to be sure to be in a Italian culture context - // where decimal char is comma. - Assert.Equal("1,5", (1.5).ToString()); - - var reporter = new CoberturaReporter(); - string report = reporter.Report(result, new Mock().Object); - - Assert.NotEmpty(report); - - var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); - - IEnumerable matchingRateAttributes = doc.Descendants().Attributes().Where(attr => attr.Name.LocalName.EndsWith("-rate")); - IEnumerable rateParentNodeNames = matchingRateAttributes.Select(attr => attr.Parent.Name.LocalName); - Assert.Contains("package", rateParentNodeNames); - Assert.Contains("class", rateParentNodeNames); - Assert.Contains("method", rateParentNodeNames); - Assert.All(matchingRateAttributes.Select(attr => attr.Value), - value => - { - Assert.DoesNotContain(",", value); - Assert.Contains(".", value); - Assert.Equal(0.5, double.Parse(value, CultureInfo.InvariantCulture)); - }); - - IEnumerable matchingComplexityAttributes = doc.Descendants().Attributes().Where(attr => attr.Name.LocalName.Equals("complexity")); - IEnumerable complexityParentNodeNames = matchingComplexityAttributes.Select(attr => attr.Parent.Name.LocalName); - Assert.Contains("package", complexityParentNodeNames); - Assert.Contains("class", complexityParentNodeNames); - Assert.Contains("method", complexityParentNodeNames); - Assert.All(matchingComplexityAttributes.Select(attr => attr.Value), - value => - { - Assert.Equal(branches.Count, int.Parse(value, CultureInfo.InvariantCulture)); - }); - } - finally - { - Thread.CurrentThread.CurrentCulture = currentCulture; - } - } - - [Theory] - [InlineData( - "Test.SearchRequest::pb::Google.Protobuf.IMessage.get_Descriptor()", - "Google.Protobuf.IMessage.get_Descriptor", - "()")] - [InlineData( - "Test.SearchRequest::pb::Google.Protobuf.IMessage.get_Descriptor(int i)", - "Google.Protobuf.IMessage.get_Descriptor", - "(int i)")] - public void TestEnsureParseMethodStringCorrectly( - string methodString, - string expectedMethodName, - string expectedSignature) + Assert.DoesNotContain(",", value); + Assert.Contains(".", value); + Assert.Equal(0.5, double.Parse(value, CultureInfo.InvariantCulture)); + }); + + IEnumerable matchingComplexityAttributes = doc.Descendants().Attributes().Where(attr => attr.Name.LocalName.Equals("complexity")); + IEnumerable complexityParentNodeNames = matchingComplexityAttributes.Select(attr => attr.Parent.Name.LocalName); + Assert.Contains("package", complexityParentNodeNames); + Assert.Contains("class", complexityParentNodeNames); + Assert.Contains("method", complexityParentNodeNames); + Assert.All(matchingComplexityAttributes.Select(attr => attr.Value), + value => { - var result = new CoverageResult(); - result.Parameters = new CoverageParameters(); - result.Identifier = Guid.NewGuid().ToString(); - - var lines = new Lines(); - lines.Add(1, 1); - - var branches = new Branches(); - branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }); - - var methods = new Methods(); - methods.Add(methodString, new Method()); - methods[methodString].Lines = lines; - methods[methodString].Branches = branches; - - var classes = new Classes(); - classes.Add("Google.Protobuf.Reflection.MessageDescriptor", methods); - - var documents = new Documents(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - documents.Add(@"C:\doc.cs", classes); - } - else - { - documents.Add(@"/doc.cs", classes); - } - - result.Modules = new Modules(); - result.Modules.Add("module", documents); - - var reporter = new CoberturaReporter(); - string report = reporter.Report(result, new Mock().Object); - - Assert.NotEmpty(report); - - var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); - var methodAttrs = doc.Descendants() - .Where(o => o.Name.LocalName == "method") - .Attributes() - .ToDictionary(o => o.Name.LocalName, o => o.Value); - Assert.Equal(expectedMethodName, methodAttrs["name"]); - Assert.Equal(expectedSignature, methodAttrs["signature"]); - } + Assert.Equal(branches.Count, int.Parse(value, CultureInfo.InvariantCulture)); + }); + } + finally + { + Thread.CurrentThread.CurrentCulture = currentCulture; + } + } - [Fact] - public void TestReportWithDifferentDirectories() - { - var result = new CoverageResult(); - result.Parameters = new CoverageParameters(); - result.Identifier = Guid.NewGuid().ToString(); - - string absolutePath1; - string absolutePath2; - string absolutePath3; - string absolutePath4; - string absolutePath5; - string absolutePath6; - string absolutePath7; - string absolutePath8; - string absolutePath9; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - absolutePath1 = @"C:\projA\dir1\dir10\file1.cs"; - absolutePath2 = @"C:\projA\dir1\dir10\file2.cs"; - absolutePath3 = @"C:\projA\dir1\file3.cs"; - absolutePath4 = @"E:\projB\dir1\dir10\file4.cs"; - absolutePath5 = @"E:\projB\dir2\file5.cs"; - absolutePath6 = @"F:\file6.cs"; - absolutePath7 = @"F:\"; - absolutePath8 = @"c:\git\coverletissue\localpackagetest\deterministicbuild\ClassLibrary1\Class1.cs"; - absolutePath9 = @"c:\git\coverletissue\localpackagetest\deterministicbuild\ClassLibrary2\Class1.cs"; - } - else - { - absolutePath1 = @"/projA/dir1/dir10/file1.cs"; - absolutePath2 = @"/projA/dir1/file2.cs"; - absolutePath3 = @"/projA/dir1/file3.cs"; - absolutePath4 = @"/projA/dir2/file4.cs"; - absolutePath5 = @"/projA/dir2/file5.cs"; - absolutePath6 = @"/file1.cs"; - absolutePath7 = @"/"; - absolutePath8 = @"/git/coverletissue/localpackagetest/deterministicbuild/ClassLibrary1/Class1.cs"; - absolutePath9 = @"/git/coverletissue/localpackagetest/deterministicbuild/ClassLibrary2/Class1.cs"; - } - - var classes = new Classes { { "Class", new Methods() } }; - var documents = new Documents { + [Theory] + [InlineData( + "Test.SearchRequest::pb::Google.Protobuf.IMessage.get_Descriptor()", + "Google.Protobuf.IMessage.get_Descriptor", + "()")] + [InlineData( + "Test.SearchRequest::pb::Google.Protobuf.IMessage.get_Descriptor(int i)", + "Google.Protobuf.IMessage.get_Descriptor", + "(int i)")] + public void TestEnsureParseMethodStringCorrectly( + string methodString, + string expectedMethodName, + string expectedSignature) + { + var result = new CoverageResult(); + result.Parameters = new CoverageParameters(); + result.Identifier = Guid.NewGuid().ToString(); + + var lines = new Lines(); + lines.Add(1, 1); + + var branches = new Branches(); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }); + + var methods = new Methods(); + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; + + var classes = new Classes(); + classes.Add("Google.Protobuf.Reflection.MessageDescriptor", methods); + + var documents = new Documents(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + documents.Add(@"C:\doc.cs", classes); + } + else + { + documents.Add(@"/doc.cs", classes); + } + + result.Modules = new Modules(); + result.Modules.Add("module", documents); + + var reporter = new CoberturaReporter(); + string report = reporter.Report(result, new Mock().Object); + + Assert.NotEmpty(report); + + var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); + var methodAttrs = doc.Descendants() + .Where(o => o.Name.LocalName == "method") + .Attributes() + .ToDictionary(o => o.Name.LocalName, o => o.Value); + Assert.Equal(expectedMethodName, methodAttrs["name"]); + Assert.Equal(expectedSignature, methodAttrs["signature"]); + } + + [Fact] + public void TestReportWithDifferentDirectories() + { + var result = new CoverageResult(); + result.Parameters = new CoverageParameters(); + result.Identifier = Guid.NewGuid().ToString(); + + string absolutePath1; + string absolutePath2; + string absolutePath3; + string absolutePath4; + string absolutePath5; + string absolutePath6; + string absolutePath7; + string absolutePath8; + string absolutePath9; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + absolutePath1 = @"C:\projA\dir1\dir10\file1.cs"; + absolutePath2 = @"C:\projA\dir1\dir10\file2.cs"; + absolutePath3 = @"C:\projA\dir1\file3.cs"; + absolutePath4 = @"E:\projB\dir1\dir10\file4.cs"; + absolutePath5 = @"E:\projB\dir2\file5.cs"; + absolutePath6 = @"F:\file6.cs"; + absolutePath7 = @"F:\"; + absolutePath8 = @"c:\git\coverletissue\localpackagetest\deterministicbuild\ClassLibrary1\Class1.cs"; + absolutePath9 = @"c:\git\coverletissue\localpackagetest\deterministicbuild\ClassLibrary2\Class1.cs"; + } + else + { + absolutePath1 = @"/projA/dir1/dir10/file1.cs"; + absolutePath2 = @"/projA/dir1/file2.cs"; + absolutePath3 = @"/projA/dir1/file3.cs"; + absolutePath4 = @"/projA/dir2/file4.cs"; + absolutePath5 = @"/projA/dir2/file5.cs"; + absolutePath6 = @"/file1.cs"; + absolutePath7 = @"/"; + absolutePath8 = @"/git/coverletissue/localpackagetest/deterministicbuild/ClassLibrary1/Class1.cs"; + absolutePath9 = @"/git/coverletissue/localpackagetest/deterministicbuild/ClassLibrary2/Class1.cs"; + } + + var classes = new Classes { { "Class", new Methods() } }; + var documents = new Documents { { absolutePath1, classes }, { absolutePath2, classes }, { absolutePath3, classes }, @@ -215,58 +215,58 @@ public void TestReportWithDifferentDirectories() { absolutePath9, classes } }; - result.Modules = new Modules { { "Module", documents } }; - - var reporter = new CoberturaReporter(); - string report = reporter.Report(result, new Mock().Object); - - var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); - - var basePaths = doc.Element("coverage").Element("sources").Elements().Select(e => e.Value).ToList(); - var relativePaths = doc.Element("coverage").Element("packages").Element("package") - .Element("classes").Elements().Select(e => e.Attribute("filename").Value).ToList(); - - var possiblePaths = new List(); - foreach (string basePath in basePaths) - { - foreach (string relativePath in relativePaths) - { - possiblePaths.Add(Path.Combine(basePath, relativePath)); - } - } - - Assert.Contains(absolutePath1, possiblePaths); - Assert.Contains(absolutePath2, possiblePaths); - Assert.Contains(absolutePath3, possiblePaths); - Assert.Contains(absolutePath4, possiblePaths); - Assert.Contains(absolutePath5, possiblePaths); - Assert.Contains(absolutePath6, possiblePaths); - Assert.Contains(absolutePath7, possiblePaths); - Assert.Contains(absolutePath8, possiblePaths); - Assert.Contains(absolutePath9, possiblePaths); - } + result.Modules = new Modules { { "Module", documents } }; + + var reporter = new CoberturaReporter(); + string report = reporter.Report(result, new Mock().Object); + + var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); - [Fact] - public void TestReportWithSourcelinkPaths() + var basePaths = doc.Element("coverage").Element("sources").Elements().Select(e => e.Value).ToList(); + var relativePaths = doc.Element("coverage").Element("packages").Element("package") + .Element("classes").Elements().Select(e => e.Attribute("filename").Value).ToList(); + + var possiblePaths = new List(); + foreach (string basePath in basePaths) + { + foreach (string relativePath in relativePaths) { - var result = new CoverageResult { Parameters = new CoverageParameters() { UseSourceLink = true }, Identifier = Guid.NewGuid().ToString() }; + possiblePaths.Add(Path.Combine(basePath, relativePath)); + } + } + + Assert.Contains(absolutePath1, possiblePaths); + Assert.Contains(absolutePath2, possiblePaths); + Assert.Contains(absolutePath3, possiblePaths); + Assert.Contains(absolutePath4, possiblePaths); + Assert.Contains(absolutePath5, possiblePaths); + Assert.Contains(absolutePath6, possiblePaths); + Assert.Contains(absolutePath7, possiblePaths); + Assert.Contains(absolutePath8, possiblePaths); + Assert.Contains(absolutePath9, possiblePaths); + } + + [Fact] + public void TestReportWithSourcelinkPaths() + { + var result = new CoverageResult { Parameters = new CoverageParameters() { UseSourceLink = true }, Identifier = Guid.NewGuid().ToString() }; - string absolutePath = - @"https://raw.githubusercontent.com/johndoe/Coverlet/02c09baa8bfdee3b6cdf4be89bd98c8157b0bc08/Demo.cs"; + string absolutePath = + @"https://raw.githubusercontent.com/johndoe/Coverlet/02c09baa8bfdee3b6cdf4be89bd98c8157b0bc08/Demo.cs"; - var classes = new Classes { { "Class", new Methods() } }; - var documents = new Documents { { absolutePath, classes } }; + var classes = new Classes { { "Class", new Methods() } }; + var documents = new Documents { { absolutePath, classes } }; - result.Modules = new Modules { { "Module", documents } }; + result.Modules = new Modules { { "Module", documents } }; - var reporter = new CoberturaReporter(); - string report = reporter.Report(result, new Mock().Object); + var reporter = new CoberturaReporter(); + string report = reporter.Report(result, new Mock().Object); - var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); - string fileName = doc.Element("coverage").Element("packages").Element("package").Element("classes").Elements() - .Select(e => e.Attribute("filename").Value).Single(); + var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); + string fileName = doc.Element("coverage").Element("packages").Element("package").Element("classes").Elements() + .Select(e => e.Attribute("filename").Value).Single(); - Assert.Equal(absolutePath, fileName); - } + Assert.Equal(absolutePath, fileName); } + } } diff --git a/test/coverlet.core.tests/Reporters/JsonReporterTests.cs b/test/coverlet.core.tests/Reporters/JsonReporterTests.cs index 316bd1cae..b1f54215f 100644 --- a/test/coverlet.core.tests/Reporters/JsonReporterTests.cs +++ b/test/coverlet.core.tests/Reporters/JsonReporterTests.cs @@ -1,9 +1,9 @@ // Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using Coverlet.Core.Abstractions; using Moq; -using System; using Xunit; namespace Coverlet.Core.Reporters.Tests diff --git a/test/coverlet.core.tests/Reporters/LcovReporterTests.cs b/test/coverlet.core.tests/Reporters/LcovReporterTests.cs index b7462f152..736730c11 100644 --- a/test/coverlet.core.tests/Reporters/LcovReporterTests.cs +++ b/test/coverlet.core.tests/Reporters/LcovReporterTests.cs @@ -1,49 +1,49 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using Coverlet.Core.Abstractions; using Moq; -using System; using Xunit; namespace Coverlet.Core.Reporters.Tests { - public class LcovReporterTests + public class LcovReporterTests + { + [Fact] + public void TestReport() { - [Fact] - public void TestReport() - { - var result = new CoverageResult(); - result.Parameters = new CoverageParameters(); - result.Identifier = Guid.NewGuid().ToString(); - - var lines = new Lines(); - lines.Add(1, 1); - lines.Add(2, 0); - - var branches = new Branches(); - branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }); - branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 23, EndOffset = 27, Path = 1, Ordinal = 2 }); - - var methods = new Methods(); - string methodString = "System.Void Coverlet.Core.Reporters.Tests.LcovReporterTests.TestReport()"; - methods.Add(methodString, new Method()); - methods[methodString].Lines = lines; - methods[methodString].Branches = branches; - - var classes = new Classes(); - classes.Add("Coverlet.Core.Reporters.Tests.LcovReporterTests", methods); - - var documents = new Documents(); - documents.Add("doc.cs", classes); - result.Modules = new Modules(); - result.Modules.Add("module", documents); - - var reporter = new LcovReporter(); - string report = reporter.Report(result, new Mock().Object); - - Assert.NotEmpty(report); - Assert.Equal("SF:doc.cs", report.Split(Environment.NewLine)[0]); - } + var result = new CoverageResult(); + result.Parameters = new CoverageParameters(); + result.Identifier = Guid.NewGuid().ToString(); + + var lines = new Lines(); + lines.Add(1, 1); + lines.Add(2, 0); + + var branches = new Branches(); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }); + branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 23, EndOffset = 27, Path = 1, Ordinal = 2 }); + + var methods = new Methods(); + string methodString = "System.Void Coverlet.Core.Reporters.Tests.LcovReporterTests.TestReport()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; + + var classes = new Classes(); + classes.Add("Coverlet.Core.Reporters.Tests.LcovReporterTests", methods); + + var documents = new Documents(); + documents.Add("doc.cs", classes); + result.Modules = new Modules(); + result.Modules.Add("module", documents); + + var reporter = new LcovReporter(); + string report = reporter.Report(result, new Mock().Object); + + Assert.NotEmpty(report); + Assert.Equal("SF:doc.cs", report.Split(Environment.NewLine)[0]); } -} \ No newline at end of file + } +} diff --git a/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs b/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs index 678ea9ae2..c5b530d7e 100644 --- a/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs +++ b/test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs @@ -1,133 +1,133 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Coverlet.Core.Abstractions; -using Moq; using System; using System.IO; using System.Linq; using System.Text; using System.Xml.Linq; +using Coverlet.Core.Abstractions; +using Moq; using Xunit; namespace Coverlet.Core.Reporters.Tests { - public class OpenCoverReporterTests + public class OpenCoverReporterTests + { + [Fact] + public void TestReport() { - [Fact] - public void TestReport() - { - var result = new CoverageResult(); - result.Parameters = new CoverageParameters(); - result.Identifier = Guid.NewGuid().ToString(); - - result.Modules = new Modules(); - result.Modules.Add("Coverlet.Core.Reporters.Tests", CreateFirstDocuments()); - - var reporter = new OpenCoverReporter(); - string report = reporter.Report(result, new Mock().Object); - Assert.NotEmpty(report); - var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); - Assert.Empty(doc.Descendants().Attributes("sequenceCoverage").Where(v => v.Value != "33.33")); - Assert.Empty(doc.Descendants().Attributes("branchCoverage").Where(v => v.Value != "25")); - Assert.Empty(doc.Descendants().Attributes("nPathComplexity").Where(v => v.Value != "4")); - } - - [Fact] - public void TestFilesHaveUniqueIdsOverMultipleModules() - { - var result = new CoverageResult(); - result.Parameters = new CoverageParameters(); - result.Identifier = Guid.NewGuid().ToString(); - - result.Modules = new Modules(); - result.Modules.Add("Coverlet.Core.Reporters.Tests", CreateFirstDocuments()); - result.Modules.Add("Some.Other.Module", CreateSecondDocuments()); - - var reporter = new OpenCoverReporter(); - string xml = reporter.Report(result, new Mock().Object); - Assert.NotEqual(string.Empty, xml); - - Assert.Contains(@"", xml); - Assert.Contains(@"", xml); - } - - [Fact] - public void TestLineBranchCoverage() - { - var result = new CoverageResult - { - Identifier = Guid.NewGuid().ToString(), - Modules = new Modules { { "Coverlet.Core.Reporters.Tests", CreateBranchCoverageDocuments() } }, - Parameters = new CoverageParameters() - }; + var result = new CoverageResult(); + result.Parameters = new CoverageParameters(); + result.Identifier = Guid.NewGuid().ToString(); + + result.Modules = new Modules(); + result.Modules.Add("Coverlet.Core.Reporters.Tests", CreateFirstDocuments()); + + var reporter = new OpenCoverReporter(); + string report = reporter.Report(result, new Mock().Object); + Assert.NotEmpty(report); + var doc = XDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(report))); + Assert.Empty(doc.Descendants().Attributes("sequenceCoverage").Where(v => v.Value != "33.33")); + Assert.Empty(doc.Descendants().Attributes("branchCoverage").Where(v => v.Value != "25")); + Assert.Empty(doc.Descendants().Attributes("nPathComplexity").Where(v => v.Value != "4")); + } - string xml = new OpenCoverReporter().Report(result, new Mock().Object); + [Fact] + public void TestFilesHaveUniqueIdsOverMultipleModules() + { + var result = new CoverageResult(); + result.Parameters = new CoverageParameters(); + result.Identifier = Guid.NewGuid().ToString(); - // Line 1: Two branches, no coverage (bec = 2, bev = 0) - Assert.Contains(@"", xml); + result.Modules = new Modules(); + result.Modules.Add("Coverlet.Core.Reporters.Tests", CreateFirstDocuments()); + result.Modules.Add("Some.Other.Module", CreateSecondDocuments()); - // Line 2: Two branches, one covered (bec = 2, bev = 1) - Assert.Contains(@"", xml); + var reporter = new OpenCoverReporter(); + string xml = reporter.Report(result, new Mock().Object); + Assert.NotEqual(string.Empty, xml); - // Line 3: Two branches, all covered (bec = 2, bev = 2) - Assert.Contains(@"", xml); + Assert.Contains(@"", xml); + Assert.Contains(@"", xml); + } - // Line 4: Three branches, two covered (bec = 3, bev = 2) - Assert.Contains(@"", xml); - } + [Fact] + public void TestLineBranchCoverage() + { + var result = new CoverageResult + { + Identifier = Guid.NewGuid().ToString(), + Modules = new Modules { { "Coverlet.Core.Reporters.Tests", CreateBranchCoverageDocuments() } }, + Parameters = new CoverageParameters() + }; - private static Documents CreateFirstDocuments() - { - var lines = new Lines(); - lines.Add(1, 1); - lines.Add(2, 0); - lines.Add(3, 0); + string xml = new OpenCoverReporter().Report(result, new Mock().Object); - var branches = new Branches(); - branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }); - branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 23, EndOffset = 27, Path = 1, Ordinal = 2 }); - branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 40, EndOffset = 41, Path = 0, Ordinal = 3 }); - branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 40, EndOffset = 44, Path = 1, Ordinal = 4 }); + // Line 1: Two branches, no coverage (bec = 2, bev = 0) + Assert.Contains(@"", xml); - var methods = new Methods(); - string methodString = "System.Void Coverlet.Core.Reporters.Tests.OpenCoverReporterTests.TestReport()"; - methods.Add(methodString, new Method()); - methods[methodString].Lines = lines; - methods[methodString].Branches = branches; + // Line 2: Two branches, one covered (bec = 2, bev = 1) + Assert.Contains(@"", xml); - var classes = new Classes(); - classes.Add("Coverlet.Core.Reporters.Tests.OpenCoverReporterTests", methods); + // Line 3: Two branches, all covered (bec = 2, bev = 2) + Assert.Contains(@"", xml); - var documents = new Documents(); - documents.Add("doc.cs", classes); + // Line 4: Three branches, two covered (bec = 3, bev = 2) + Assert.Contains(@"", xml); + } - return documents; - } + private static Documents CreateFirstDocuments() + { + var lines = new Lines(); + lines.Add(1, 1); + lines.Add(2, 0); + lines.Add(3, 0); + + var branches = new Branches(); + branches.Add(new BranchInfo { Line = 1, Hits = 1, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1 }); + branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 23, EndOffset = 27, Path = 1, Ordinal = 2 }); + branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 40, EndOffset = 41, Path = 0, Ordinal = 3 }); + branches.Add(new BranchInfo { Line = 1, Hits = 0, Offset = 40, EndOffset = 44, Path = 1, Ordinal = 4 }); + + var methods = new Methods(); + string methodString = "System.Void Coverlet.Core.Reporters.Tests.OpenCoverReporterTests.TestReport()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; + + var classes = new Classes(); + classes.Add("Coverlet.Core.Reporters.Tests.OpenCoverReporterTests", methods); + + var documents = new Documents(); + documents.Add("doc.cs", classes); + + return documents; + } - private static Documents CreateSecondDocuments() - { - var lines = new Lines(); - lines.Add(1, 1); - lines.Add(2, 0); + private static Documents CreateSecondDocuments() + { + var lines = new Lines(); + lines.Add(1, 1); + lines.Add(2, 0); - var methods = new Methods(); - string methodString = "System.Void Some.Other.Module.TestClass.TestMethod()"; - methods.Add(methodString, new Method()); - methods[methodString].Lines = lines; + var methods = new Methods(); + string methodString = "System.Void Some.Other.Module.TestClass.TestMethod()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; - var classes2 = new Classes(); - classes2.Add("Some.Other.Module.TestClass", methods); + var classes2 = new Classes(); + classes2.Add("Some.Other.Module.TestClass", methods); - var documents = new Documents(); - documents.Add("TestClass.cs", classes2); + var documents = new Documents(); + documents.Add("TestClass.cs", classes2); - return documents; - } + return documents; + } - private static Documents CreateBranchCoverageDocuments() - { - var lines = new Lines + private static Documents CreateBranchCoverageDocuments() + { + var lines = new Lines { {1, 1}, {2, 1}, @@ -135,7 +135,7 @@ private static Documents CreateBranchCoverageDocuments() {4, 1}, }; - var branches = new Branches + var branches = new Branches { // Two branches, no coverage new BranchInfo {Line = 1, Hits = 0, Offset = 23, EndOffset = 24, Path = 0, Ordinal = 1}, @@ -155,16 +155,16 @@ private static Documents CreateBranchCoverageDocuments() new BranchInfo {Line = 4, Hits = 0, Offset = 40, EndOffset = 44, Path = 1, Ordinal = 4} }; - const string methodString = "System.Void Coverlet.Core.Reporters.Tests.OpenCoverReporterTests.TestReport()"; - var methods = new Methods + const string methodString = "System.Void Coverlet.Core.Reporters.Tests.OpenCoverReporterTests.TestReport()"; + var methods = new Methods { {methodString, new Method { Lines = lines, Branches = branches}} }; - return new Documents + return new Documents { {"doc.cs", new Classes {{"Coverlet.Core.Reporters.Tests.OpenCoverReporterTests", methods}}} }; - } } -} \ No newline at end of file + } +} diff --git a/test/coverlet.core.tests/Reporters/ReporterFactoryTests.cs b/test/coverlet.core.tests/Reporters/ReporterFactoryTests.cs index 1b0f7ce04..74ccbeecd 100644 --- a/test/coverlet.core.tests/Reporters/ReporterFactoryTests.cs +++ b/test/coverlet.core.tests/Reporters/ReporterFactoryTests.cs @@ -1,21 +1,21 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Xunit; namespace Coverlet.Core.Reporters.Tests { - public class ReporterFactoryTests + public class ReporterFactoryTests + { + [Fact] + public void TestCreateReporter() { - [Fact] - public void TestCreateReporter() - { - Assert.Equal(typeof(JsonReporter), new ReporterFactory("json").CreateReporter().GetType()); - Assert.Equal(typeof(LcovReporter), new ReporterFactory("lcov").CreateReporter().GetType()); - Assert.Equal(typeof(OpenCoverReporter), new ReporterFactory("opencover").CreateReporter().GetType()); - Assert.Equal(typeof(CoberturaReporter), new ReporterFactory("cobertura").CreateReporter().GetType()); - Assert.Equal(typeof(TeamCityReporter), new ReporterFactory("teamcity").CreateReporter().GetType()); - Assert.Null(new ReporterFactory("").CreateReporter()); - } + Assert.Equal(typeof(JsonReporter), new ReporterFactory("json").CreateReporter().GetType()); + Assert.Equal(typeof(LcovReporter), new ReporterFactory("lcov").CreateReporter().GetType()); + Assert.Equal(typeof(OpenCoverReporter), new ReporterFactory("opencover").CreateReporter().GetType()); + Assert.Equal(typeof(CoberturaReporter), new ReporterFactory("cobertura").CreateReporter().GetType()); + Assert.Equal(typeof(TeamCityReporter), new ReporterFactory("teamcity").CreateReporter().GetType()); + Assert.Null(new ReporterFactory("").CreateReporter()); } -} \ No newline at end of file + } +} diff --git a/test/coverlet.core.tests/Reporters/TeamCityReporter.cs b/test/coverlet.core.tests/Reporters/TeamCityReporter.cs index 95fb2642f..1d628ebde 100644 --- a/test/coverlet.core.tests/Reporters/TeamCityReporter.cs +++ b/test/coverlet.core.tests/Reporters/TeamCityReporter.cs @@ -8,21 +8,21 @@ namespace Coverlet.Core.Reporters.Tests { - public class TestCreateReporterTests - { - private readonly CoverageResult _result; - private readonly TeamCityReporter _reporter; + public class TestCreateReporterTests + { + private readonly CoverageResult _result; + private readonly TeamCityReporter _reporter; - public TestCreateReporterTests() - { - _reporter = new TeamCityReporter(); - _result = new CoverageResult(); - _result.Parameters = new CoverageParameters(); - _result.Identifier = Guid.NewGuid().ToString(); + public TestCreateReporterTests() + { + _reporter = new TeamCityReporter(); + _result = new CoverageResult(); + _result.Parameters = new CoverageParameters(); + _result.Identifier = Guid.NewGuid().ToString(); - var lines = new Lines { { 1, 1 }, { 2, 0 } }; + var lines = new Lines { { 1, 1 }, { 2, 0 } }; - var branches = new Branches + var branches = new Branches { new BranchInfo { @@ -53,81 +53,81 @@ public TestCreateReporterTests() } }; - var methods = new Methods(); - string methodString = "System.Void Coverlet.Core.Reporters.Tests.CoberturaReporterTests::TestReport()"; - methods.Add(methodString, new Method()); - methods[methodString].Lines = lines; - methods[methodString].Branches = branches; - - var classes = new Classes { { "Coverlet.Core.Reporters.Tests.CoberturaReporterTests", methods } }; - - var documents = new Documents { { "doc.cs", classes } }; - - _result.Modules = new Modules { { "module", documents } }; - } - - [Fact] - public void OutputType_IsConsoleOutputType() - { - // Assert - Assert.Equal(ReporterOutputType.Console, _reporter.OutputType); - } - - [Fact] - public void Format_IsExpectedValue() - { - // Assert - Assert.Equal("teamcity", _reporter.Format); - } - - [Fact] - public void Format_IsNull() - { - // Assert - Assert.Null(_reporter.Extension); - } - - [Fact] - public void Report_ReturnsNonNullString() - { - // Act - string output = _reporter.Report(_result, new Mock().Object); - - // Assert - Assert.False(string.IsNullOrWhiteSpace(output), "Output is not null or whitespace"); - } - - [Fact] - public void Report_ReportsLineCoverage() - { - // Act - string output = _reporter.Report(_result, new Mock().Object); - - // Assert - Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsLCovered' value='1']", output); - Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsLTotal' value='2']", output); - } - - [Fact] - public void Report_ReportsBranchCoverage() - { - // Act - string output = _reporter.Report(_result, new Mock().Object); - - // Assert - Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsBCovered' value='1']", output); - Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsBTotal' value='3']", output); - } - - [Fact] - public void Report_ReportsMethodCoverage() - { - // Act - string output = _reporter.Report(_result, new Mock().Object); - - // Assert - Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsMCovered' value='1']", output); - Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsMTotal' value='1']", output); - } + var methods = new Methods(); + string methodString = "System.Void Coverlet.Core.Reporters.Tests.CoberturaReporterTests::TestReport()"; + methods.Add(methodString, new Method()); + methods[methodString].Lines = lines; + methods[methodString].Branches = branches; + + var classes = new Classes { { "Coverlet.Core.Reporters.Tests.CoberturaReporterTests", methods } }; + + var documents = new Documents { { "doc.cs", classes } }; + + _result.Modules = new Modules { { "module", documents } }; + } + + [Fact] + public void OutputType_IsConsoleOutputType() + { + // Assert + Assert.Equal(ReporterOutputType.Console, _reporter.OutputType); + } + + [Fact] + public void Format_IsExpectedValue() + { + // Assert + Assert.Equal("teamcity", _reporter.Format); + } + + [Fact] + public void Format_IsNull() + { + // Assert + Assert.Null(_reporter.Extension); + } + + [Fact] + public void Report_ReturnsNonNullString() + { + // Act + string output = _reporter.Report(_result, new Mock().Object); + + // Assert + Assert.False(string.IsNullOrWhiteSpace(output), "Output is not null or whitespace"); + } + + [Fact] + public void Report_ReportsLineCoverage() + { + // Act + string output = _reporter.Report(_result, new Mock().Object); + + // Assert + Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsLCovered' value='1']", output); + Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsLTotal' value='2']", output); + } + + [Fact] + public void Report_ReportsBranchCoverage() + { + // Act + string output = _reporter.Report(_result, new Mock().Object); + + // Assert + Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsBCovered' value='1']", output); + Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsBTotal' value='3']", output); + } + + [Fact] + public void Report_ReportsMethodCoverage() + { + // Act + string output = _reporter.Report(_result, new Mock().Object); + + // Assert + Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsMCovered' value='1']", output); + Assert.Contains("##teamcity[buildStatisticValue key='CodeCoverageAbsMTotal' value='1']", output); } + } } diff --git a/test/coverlet.core.tests/Samples/Instrumentation.CatchBlock.cs b/test/coverlet.core.tests/Samples/Instrumentation.CatchBlock.cs index cab0e6eaa..21175a29b 100644 --- a/test/coverlet.core.tests/Samples/Instrumentation.CatchBlock.cs +++ b/test/coverlet.core.tests/Samples/Instrumentation.CatchBlock.cs @@ -301,7 +301,7 @@ public async Task ParseAsync_WithNestedCatch(string str, bool condition) { return int.Parse(str); } - catch + catch { await Task.Delay(0); if (condition) @@ -317,7 +317,7 @@ public async Task ParseAsync_WithNestedCatch(string str, bool condition) } catch { - await Task.Delay(0); + await Task.Delay(0); throw; } } diff --git a/test/coverlet.core.tests/Samples/Instrumentation.DoesNotReturn.cs b/test/coverlet.core.tests/Samples/Instrumentation.DoesNotReturn.cs index 75512c304..2fd37bff7 100644 --- a/test/coverlet.core.tests/Samples/Instrumentation.DoesNotReturn.cs +++ b/test/coverlet.core.tests/Samples/Instrumentation.DoesNotReturn.cs @@ -175,7 +175,7 @@ public void FiltersAndFinallies() Throws(); System.Console.WriteLine("Constant-2"); //unreachable } //unreachable - catch (System.InvalidOperationException e) + catch (System.InvalidOperationException e) when (e.Message != null) { System.Console.WriteLine("InCatch-1"); diff --git a/test/coverlet.core.tests/Samples/Instrumentation.ExcludeFromCoverage.NestedStateMachines.cs b/test/coverlet.core.tests/Samples/Instrumentation.ExcludeFromCoverage.NestedStateMachines.cs index d1449ad79..9b9e97320 100644 --- a/test/coverlet.core.tests/Samples/Instrumentation.ExcludeFromCoverage.NestedStateMachines.cs +++ b/test/coverlet.core.tests/Samples/Instrumentation.ExcludeFromCoverage.NestedStateMachines.cs @@ -14,5 +14,5 @@ public int Test() { return 0; } - } + } } \ No newline at end of file diff --git a/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs b/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs index 8b6e46c6d..8f74222cb 100644 --- a/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs +++ b/test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs @@ -4,448 +4,448 @@ using System.IO; using System.Linq; using System.Reflection; -using Xunit; -using Coverlet.Core.Samples.Tests; using coverlet.tests.projectsample.netframework; +using Coverlet.Core.Samples.Tests; using Mono.Cecil; using Mono.Cecil.Cil; +using Xunit; namespace Coverlet.Core.Symbols.Tests { - public class CecilSymbolHelperTests + public class CecilSymbolHelperTests + { + private ModuleDefinition _module; + private readonly CecilSymbolHelper _cecilSymbolHelper; + private readonly DefaultAssemblyResolver _resolver; + private readonly ReaderParameters _parameters; + + public CecilSymbolHelperTests() + { + string location = GetType().Assembly.Location; + _resolver = new DefaultAssemblyResolver(); + _resolver.AddSearchDirectory(Path.GetDirectoryName(location)); + _parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = _resolver }; + _module = ModuleDefinition.ReadModule(location, _parameters); + _cecilSymbolHelper = new CecilSymbolHelper(); + } + + [Fact] + public void GetBranchPoints_OneBranch() + { + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSingleDecision)}")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(2, points.Count); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(0, points[0].Path); + Assert.Equal(1, points[1].Path); + Assert.Equal(22, points[0].StartLine); + Assert.Equal(22, points[1].StartLine); + Assert.NotNull(points[1].Document); + Assert.Equal(points[0].Document, points[1].Document); + } + + [Fact] + public void GetBranchPoints_Using_Where_GeneratedBranchesIgnored() + { + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSimpleUsingStatement)}")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + Assert.Equal(2, points.Count); + } + + [Fact] + public void GetBranchPoints_GeneratedBranches_DueToCachedAnonymousMethodDelegate_Ignored() + { + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSimpleTaskWithLambda)}")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_TwoBranch() + { + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasTwoDecisions)}")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(4, points.Count); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[2].Offset, points[3].Offset); + Assert.Equal(28, points[0].StartLine); + Assert.Equal(29, points[2].StartLine); + } + + [Fact] + public void GetBranchPoints_CompleteIf() { - private ModuleDefinition _module; - private readonly CecilSymbolHelper _cecilSymbolHelper; - private readonly DefaultAssemblyResolver _resolver; - private readonly ReaderParameters _parameters; - - public CecilSymbolHelperTests() - { - string location = GetType().Assembly.Location; - _resolver = new DefaultAssemblyResolver(); - _resolver.AddSearchDirectory(Path.GetDirectoryName(location)); - _parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = _resolver }; - _module = ModuleDefinition.ReadModule(location, _parameters); - _cecilSymbolHelper = new CecilSymbolHelper(); - } - - [Fact] - public void GetBranchPoints_OneBranch() - { - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSingleDecision)}")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.NotNull(points); - Assert.Equal(2, points.Count); - Assert.Equal(points[0].Offset, points[1].Offset); - Assert.Equal(0, points[0].Path); - Assert.Equal(1, points[1].Path); - Assert.Equal(22, points[0].StartLine); - Assert.Equal(22, points[1].StartLine); - Assert.NotNull(points[1].Document); - Assert.Equal(points[0].Document, points[1].Document); - } - - [Fact] - public void GetBranchPoints_Using_Where_GeneratedBranchesIgnored() - { - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSimpleUsingStatement)}")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - Assert.Equal(2, points.Count); - } - - [Fact] - public void GetBranchPoints_GeneratedBranches_DueToCachedAnonymousMethodDelegate_Ignored() - { - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSimpleTaskWithLambda)}")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - Assert.Empty(points); - } - - [Fact] - public void GetBranchPoints_TwoBranch() - { - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasTwoDecisions)}")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.NotNull(points); - Assert.Equal(4, points.Count); - Assert.Equal(points[0].Offset, points[1].Offset); - Assert.Equal(points[2].Offset, points[3].Offset); - Assert.Equal(28, points[0].StartLine); - Assert.Equal(29, points[2].StartLine); - } - - [Fact] - public void GetBranchPoints_CompleteIf() - { - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasCompleteIf)}")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.NotNull(points); - Assert.Equal(2, points.Count); - Assert.Equal(points[0].Offset, points[1].Offset); - Assert.Equal(35, points[0].StartLine); - Assert.Equal(35, points[1].StartLine); - } + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasCompleteIf)}")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(2, points.Count); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(35, points[0].StartLine); + Assert.Equal(35, points[1].StartLine); + } #if !RELEASE // Issue https://github.com/tonerdo/coverlet/issues/389 - [Fact] - public void GetBranchPoints_Switch() - { - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitch)}")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.NotNull(points); - Assert.Equal(4, points.Count); - Assert.Equal(points[0].Offset, points[1].Offset); - Assert.Equal(points[0].Offset, points[2].Offset); - Assert.Equal(3, points[3].Path); - - Assert.Equal(47, points[0].StartLine); - Assert.Equal(47, points[1].StartLine); - Assert.Equal(47, points[2].StartLine); - Assert.Equal(47, points[3].StartLine); - } - - [Fact] - public void GetBranchPoints_SwitchWithDefault() - { - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitchWithDefault)}")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.NotNull(points); - Assert.Equal(4, points.Count); - Assert.Equal(points[0].Offset, points[1].Offset); - Assert.Equal(points[0].Offset, points[2].Offset); - Assert.Equal(3, points[3].Path); - - Assert.Equal(61, points[0].StartLine); - Assert.Equal(61, points[1].StartLine); - Assert.Equal(61, points[2].StartLine); - Assert.Equal(61, points[3].StartLine); - } - - [Fact] - public void GetBranchPoints_SwitchWithBreaks() - { - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitchWithBreaks)}")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.NotNull(points); - Assert.Equal(4, points.Count); - Assert.Equal(points[0].Offset, points[1].Offset); - Assert.Equal(points[0].Offset, points[2].Offset); - Assert.Equal(3, points[3].Path); - - Assert.Equal(77, points[0].StartLine); - Assert.Equal(77, points[1].StartLine); - Assert.Equal(77, points[2].StartLine); - Assert.Equal(77, points[3].StartLine); - } - - [Fact] - public void GetBranchPoints_SwitchWithMultipleCases() - { - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitchWithMultipleCases)}")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.NotNull(points); - Assert.Equal(4, points.Count); - Assert.Equal(points[0].Offset, points[1].Offset); - Assert.Equal(points[0].Offset, points[2].Offset); - Assert.Equal(points[0].Offset, points[3].Offset); - Assert.Equal(3, points[3].Path); - - Assert.Equal(95, points[0].StartLine); - Assert.Equal(95, points[1].StartLine); - Assert.Equal(95, points[2].StartLine); - Assert.Equal(95, points[3].StartLine); - } + [Fact] + public void GetBranchPoints_Switch() + { + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitch)}")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(4, points.Count); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[0].Offset, points[2].Offset); + Assert.Equal(3, points[3].Path); + + Assert.Equal(47, points[0].StartLine); + Assert.Equal(47, points[1].StartLine); + Assert.Equal(47, points[2].StartLine); + Assert.Equal(47, points[3].StartLine); + } + + [Fact] + public void GetBranchPoints_SwitchWithDefault() + { + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitchWithDefault)}")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(4, points.Count); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[0].Offset, points[2].Offset); + Assert.Equal(3, points[3].Path); + + Assert.Equal(61, points[0].StartLine); + Assert.Equal(61, points[1].StartLine); + Assert.Equal(61, points[2].StartLine); + Assert.Equal(61, points[3].StartLine); + } + + [Fact] + public void GetBranchPoints_SwitchWithBreaks() + { + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitchWithBreaks)}")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(4, points.Count); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[0].Offset, points[2].Offset); + Assert.Equal(3, points[3].Path); + + Assert.Equal(77, points[0].StartLine); + Assert.Equal(77, points[1].StartLine); + Assert.Equal(77, points[2].StartLine); + Assert.Equal(77, points[3].StartLine); + } + + [Fact] + public void GetBranchPoints_SwitchWithMultipleCases() + { + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.HasSwitchWithMultipleCases)}")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + Assert.Equal(4, points.Count); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[0].Offset, points[2].Offset); + Assert.Equal(points[0].Offset, points[3].Offset); + Assert.Equal(3, points[3].Path); + + Assert.Equal(95, points[0].StartLine); + Assert.Equal(95, points[1].StartLine); + Assert.Equal(95, points[2].StartLine); + Assert.Equal(95, points[3].StartLine); + } #endif - [Fact] - public void GetBranchPoints_AssignsNegativeLineNumberToBranchesInMethodsThatHaveNoInstrumentablePoints() - { - /* - * Yes these actually exist - the compiler is very inventive - * in this case for an anonymous class the compiler will dynamically create an Equals 'utility' method. - */ - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName.Contains("f__AnonymousType")); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains("::Equals")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.NotNull(points); - foreach (BranchPoint branchPoint in points) - Assert.Equal(-1, branchPoint.StartLine); - } - - [Fact] - public void GetBranchPoints_UsingWithException_Issue243_IgnoresBranchInFinallyBlock() - { - // arrange - TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); - MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.UsingWithException_Issue243)}")); - - // check that the method is laid out the way we discovered it to be during the defect - // @see https://github.com/OpenCover/opencover/issues/243 - Assert.Single(method.Body.ExceptionHandlers); - Assert.NotNull(method.Body.ExceptionHandlers[0].HandlerStart); - Assert.Null(method.Body.ExceptionHandlers[0].HandlerEnd); - Assert.Equal(1, method.Body.Instructions.Count(i => i.OpCode.FlowControl == FlowControl.Cond_Branch)); - Assert.True(method.Body.Instructions.First(i => i.OpCode.FlowControl == FlowControl.Cond_Branch).Offset > method.Body.ExceptionHandlers[0].HandlerStart.Offset); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.Empty(points); - } - - [Fact] - public void GetBranchPoints_IgnoresSwitchIn_GeneratedMoveNext() - { - // arrange - string nestedName = typeof(Iterator).GetNestedTypes(BindingFlags.NonPublic).First().Name; - TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(Iterator).FullName); - TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); - MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.Empty(points); - } - - [Fact] - public void GetBranchPoints_IgnoresBranchesIn_GeneratedMoveNextForSingletonIterator() - { - // arrange - string nestedName = typeof(SingletonIterator).GetNestedTypes(BindingFlags.NonPublic).First().Name; - TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(SingletonIterator).FullName); - TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); - MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.Empty(points); - } - - [Fact] - public void GetBranchPoints_IgnoresBranchesIn_AsyncAwaitStateMachine() - { - // arrange - string nestedName = typeof(AsyncAwaitStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; - TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AsyncAwaitStateMachine).FullName); - TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); - MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.Empty(points); - } - - [Fact] - public void GetBranchPoints_IgnoresBranchesIn_AsyncAwaitStateMachineNetFramework() - { - // arrange - string location = Directory.GetFiles(Directory.GetCurrentDirectory(), "coverlet.tests.projectsample.netframework.dll").First(); - _resolver.AddSearchDirectory(Path.GetDirectoryName(location)); - _module = ModuleDefinition.ReadModule(location, _parameters); - - string nestedName = typeof(AsyncAwaitStateMachineNetFramework).GetNestedTypes(BindingFlags.NonPublic).First().Name; - TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AsyncAwaitStateMachineNetFramework).FullName); - TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); - MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.Empty(points); - } - - [Fact] - public void GetBranchPoints_IgnoresBranchesIn_AsyncAwaitValueTaskStateMachine() - { - // arrange - string nestedName = typeof(AsyncAwaitValueTaskStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; - TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AsyncAwaitValueTaskStateMachine).FullName); - TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); - MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.Empty(points); - } - - [Fact] - public void GetBranchPoints_IgnoresMostBranchesIn_AwaitForeachStateMachine() - { - // arrange - string nestedName = typeof(AwaitForeachStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; - TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AwaitForeachStateMachine).FullName); - TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); - MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - // We do expect there to be a two-way branch (stay in the loop or not?) on - // the line containing "await foreach". - Assert.NotNull(points); - Assert.Equal(2, points.Count); - Assert.Equal(points[0].Offset, points[1].Offset); - Assert.Equal(204, points[0].StartLine); - Assert.Equal(204, points[1].StartLine); - } - - [Fact] - public void GetBranchPoints_IgnoresMostBranchesIn_AwaitForeachStateMachine_WithBranchesWithinIt() - { - // arrange - string nestedName = typeof(AwaitForeachStateMachine_WithBranches).GetNestedTypes(BindingFlags.NonPublic).First().Name; - TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AwaitForeachStateMachine_WithBranches).FullName); - TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); - MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - // We do expect there to be four branch points (two places where we can branch - // two ways), one being the "stay in the loop or not?" branch on the line - // containing "await foreach" and the other being the "if" statement inside - // the loop. - Assert.NotNull(points); - Assert.Equal(4, points.Count); - Assert.Equal(points[0].Offset, points[1].Offset); - Assert.Equal(points[2].Offset, points[3].Offset); - Assert.Equal(219, points[0].StartLine); - Assert.Equal(219, points[1].StartLine); - Assert.Equal(217, points[2].StartLine); - Assert.Equal(217, points[3].StartLine); - } - - [Fact] - public void GetBranchPoints_IgnoresExtraBranchesIn_AsyncIteratorStateMachine() - { - // arrange - string nestedName = typeof(AsyncIteratorStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; - TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AsyncIteratorStateMachine).FullName); - TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); - MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - // We do expect the "for" loop to be a branch with two branch points, but that's it. - Assert.NotNull(points); - Assert.Equal(2, points.Count); - Assert.Equal(237, points[0].StartLine); - Assert.Equal(237, points[1].StartLine); - } - - [Fact] - public void GetBranchPoints_IgnoreBranchesIn_AwaitUsingStateMachine() - { - // arrange - string nestedName = typeof(AwaitUsingStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; - TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AwaitUsingStateMachine).FullName); - TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); - MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.Empty(points); - } - - [Fact] - public void GetBranchPoints_IgnoreBranchesIn_ScopedAwaitUsingStateMachine() - { - // arrange - string nestedName = typeof(ScopedAwaitUsingStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; - TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(ScopedAwaitUsingStateMachine).FullName); - TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); - MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); - - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - // assert - Assert.Empty(points); - } - - [Fact] - public void GetBranchPoints_ExceptionFilter() - { - // arrange - TypeDefinition type = _module.Types.Single(x => x.FullName == typeof(ExceptionFilter).FullName); - MethodDefinition method = type.Methods.Single(x => x.FullName.Contains($"::{nameof(ExceptionFilter.Test)}")); - // act - System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); - - Assert.Empty(points); - } + [Fact] + public void GetBranchPoints_AssignsNegativeLineNumberToBranchesInMethodsThatHaveNoInstrumentablePoints() + { + /* + * Yes these actually exist - the compiler is very inventive + * in this case for an anonymous class the compiler will dynamically create an Equals 'utility' method. + */ + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName.Contains("f__AnonymousType")); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains("::Equals")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.NotNull(points); + foreach (BranchPoint branchPoint in points) + Assert.Equal(-1, branchPoint.StartLine); + } + + [Fact] + public void GetBranchPoints_UsingWithException_Issue243_IgnoresBranchInFinallyBlock() + { + // arrange + TypeDefinition type = _module.Types.First(x => x.FullName == typeof(DeclaredConstructorClass).FullName); + MethodDefinition method = type.Methods.First(x => x.FullName.Contains($"::{nameof(DeclaredConstructorClass.UsingWithException_Issue243)}")); + + // check that the method is laid out the way we discovered it to be during the defect + // @see https://github.com/OpenCover/opencover/issues/243 + Assert.Single(method.Body.ExceptionHandlers); + Assert.NotNull(method.Body.ExceptionHandlers[0].HandlerStart); + Assert.Null(method.Body.ExceptionHandlers[0].HandlerEnd); + Assert.Equal(1, method.Body.Instructions.Count(i => i.OpCode.FlowControl == FlowControl.Cond_Branch)); + Assert.True(method.Body.Instructions.First(i => i.OpCode.FlowControl == FlowControl.Cond_Branch).Offset > method.Body.ExceptionHandlers[0].HandlerStart.Offset); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_IgnoresSwitchIn_GeneratedMoveNext() + { + // arrange + string nestedName = typeof(Iterator).GetNestedTypes(BindingFlags.NonPublic).First().Name; + TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(Iterator).FullName); + TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_IgnoresBranchesIn_GeneratedMoveNextForSingletonIterator() + { + // arrange + string nestedName = typeof(SingletonIterator).GetNestedTypes(BindingFlags.NonPublic).First().Name; + TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(SingletonIterator).FullName); + TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_IgnoresBranchesIn_AsyncAwaitStateMachine() + { + // arrange + string nestedName = typeof(AsyncAwaitStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; + TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AsyncAwaitStateMachine).FullName); + TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_IgnoresBranchesIn_AsyncAwaitStateMachineNetFramework() + { + // arrange + string location = Directory.GetFiles(Directory.GetCurrentDirectory(), "coverlet.tests.projectsample.netframework.dll").First(); + _resolver.AddSearchDirectory(Path.GetDirectoryName(location)); + _module = ModuleDefinition.ReadModule(location, _parameters); + + string nestedName = typeof(AsyncAwaitStateMachineNetFramework).GetNestedTypes(BindingFlags.NonPublic).First().Name; + TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AsyncAwaitStateMachineNetFramework).FullName); + TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_IgnoresBranchesIn_AsyncAwaitValueTaskStateMachine() + { + // arrange + string nestedName = typeof(AsyncAwaitValueTaskStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; + TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AsyncAwaitValueTaskStateMachine).FullName); + TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_IgnoresMostBranchesIn_AwaitForeachStateMachine() + { + // arrange + string nestedName = typeof(AwaitForeachStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; + TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AwaitForeachStateMachine).FullName); + TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + // We do expect there to be a two-way branch (stay in the loop or not?) on + // the line containing "await foreach". + Assert.NotNull(points); + Assert.Equal(2, points.Count); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(204, points[0].StartLine); + Assert.Equal(204, points[1].StartLine); + } + + [Fact] + public void GetBranchPoints_IgnoresMostBranchesIn_AwaitForeachStateMachine_WithBranchesWithinIt() + { + // arrange + string nestedName = typeof(AwaitForeachStateMachine_WithBranches).GetNestedTypes(BindingFlags.NonPublic).First().Name; + TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AwaitForeachStateMachine_WithBranches).FullName); + TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + // We do expect there to be four branch points (two places where we can branch + // two ways), one being the "stay in the loop or not?" branch on the line + // containing "await foreach" and the other being the "if" statement inside + // the loop. + Assert.NotNull(points); + Assert.Equal(4, points.Count); + Assert.Equal(points[0].Offset, points[1].Offset); + Assert.Equal(points[2].Offset, points[3].Offset); + Assert.Equal(219, points[0].StartLine); + Assert.Equal(219, points[1].StartLine); + Assert.Equal(217, points[2].StartLine); + Assert.Equal(217, points[3].StartLine); + } + + [Fact] + public void GetBranchPoints_IgnoresExtraBranchesIn_AsyncIteratorStateMachine() + { + // arrange + string nestedName = typeof(AsyncIteratorStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; + TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AsyncIteratorStateMachine).FullName); + TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + // We do expect the "for" loop to be a branch with two branch points, but that's it. + Assert.NotNull(points); + Assert.Equal(2, points.Count); + Assert.Equal(237, points[0].StartLine); + Assert.Equal(237, points[1].StartLine); + } + + [Fact] + public void GetBranchPoints_IgnoreBranchesIn_AwaitUsingStateMachine() + { + // arrange + string nestedName = typeof(AwaitUsingStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; + TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AwaitUsingStateMachine).FullName); + TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_IgnoreBranchesIn_ScopedAwaitUsingStateMachine() + { + // arrange + string nestedName = typeof(ScopedAwaitUsingStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name; + TypeDefinition type = _module.Types.FirstOrDefault(x => x.FullName == typeof(ScopedAwaitUsingStateMachine).FullName); + TypeDefinition nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName)); + MethodDefinition method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()")); + + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + // assert + Assert.Empty(points); + } + + [Fact] + public void GetBranchPoints_ExceptionFilter() + { + // arrange + TypeDefinition type = _module.Types.Single(x => x.FullName == typeof(ExceptionFilter).FullName); + MethodDefinition method = type.Methods.Single(x => x.FullName.Contains($"::{nameof(ExceptionFilter.Test)}")); + // act + System.Collections.Generic.IReadOnlyList points = _cecilSymbolHelper.GetBranchPoints(method); + + Assert.Empty(points); } + } } diff --git a/test/coverlet.integration.tests/AssertHelper.cs b/test/coverlet.integration.tests/AssertHelper.cs index a8ab085db..e93193051 100644 --- a/test/coverlet.integration.tests/AssertHelper.cs +++ b/test/coverlet.integration.tests/AssertHelper.cs @@ -10,93 +10,93 @@ namespace Coverlet.Integration.Tests { - internal static class AssertHelper + internal static class AssertHelper + { + public static Classes Document(this Modules modules, string docName) { - public static Classes Document(this Modules modules, string docName) - { - if (docName is null) - { - throw new ArgumentNullException(nameof(docName)); - } - - foreach (KeyValuePair module in modules) - { - foreach (KeyValuePair document in module.Value) - { - if (Path.GetFileName(document.Key) == docName) - { - return document.Value; - } - } - } + if (docName is null) + { + throw new ArgumentNullException(nameof(docName)); + } - throw new XunitException($"Document not found '{docName}'"); + foreach (KeyValuePair module in modules) + { + foreach (KeyValuePair document in module.Value) + { + if (Path.GetFileName(document.Key) == docName) + { + return document.Value; + } } + } - public static Methods Class(this Classes classes, string className) - { - if (className is null) - { - throw new ArgumentNullException(nameof(className)); - } + throw new XunitException($"Document not found '{docName}'"); + } - foreach (KeyValuePair @class in classes) - { - if (@class.Key == className) - { - return @class.Value; - } - } + public static Methods Class(this Classes classes, string className) + { + if (className is null) + { + throw new ArgumentNullException(nameof(className)); + } - throw new XunitException($"Document not found '{className}'"); + foreach (KeyValuePair @class in classes) + { + if (@class.Key == className) + { + return @class.Value; } + } - public static Method Method(this Methods methods, string methodName) - { - if (methodName is null) - { - throw new ArgumentNullException(nameof(methodName)); - } + throw new XunitException($"Document not found '{className}'"); + } - foreach (KeyValuePair method in methods) - { - if (method.Key.Contains(methodName)) - { - return method.Value; - } - } + public static Method Method(this Methods methods, string methodName) + { + if (methodName is null) + { + throw new ArgumentNullException(nameof(methodName)); + } - throw new XunitException($"Document not found '{methodName}'"); + foreach (KeyValuePair method in methods) + { + if (method.Key.Contains(methodName)) + { + return method.Value; } + } - public static void AssertLinesCovered(this Method method, params (int line, int hits)[] lines) - { - if (lines is null) - { - throw new ArgumentNullException(nameof(lines)); - } + throw new XunitException($"Document not found '{methodName}'"); + } - var linesToCover = new List(lines.Select(l => l.line)); + public static void AssertLinesCovered(this Method method, params (int line, int hits)[] lines) + { + if (lines is null) + { + throw new ArgumentNullException(nameof(lines)); + } - foreach (KeyValuePair line in method.Lines) - { - foreach ((int lineToCheck, int expectedHits) in lines) - { - if (line.Key == lineToCheck) - { - linesToCover.Remove(line.Key); - if (line.Value != expectedHits) - { - throw new XunitException($"Unexpected hits expected line: {lineToCheck} hits: {expectedHits} actual hits: {line.Value}"); - } - } - } - } + var linesToCover = new List(lines.Select(l => l.line)); - if (linesToCover.Count != 0) + foreach (KeyValuePair line in method.Lines) + { + foreach ((int lineToCheck, int expectedHits) in lines) + { + if (line.Key == lineToCheck) + { + linesToCover.Remove(line.Key); + if (line.Value != expectedHits) { - throw new XunitException($"Not all requested line found, {linesToCover.Select(l => l.ToString()).Aggregate((a, b) => $"{a}, {b}")}"); + throw new XunitException($"Unexpected hits expected line: {lineToCheck} hits: {expectedHits} actual hits: {line.Value}"); } + } } + } + + if (linesToCover.Count != 0) + { + throw new XunitException($"Not all requested line found, {linesToCover.Select(l => l.ToString()).Aggregate((a, b) => $"{a}, {b}")}"); + } } + } } diff --git a/test/coverlet.integration.tests/BaseTest.cs b/test/coverlet.integration.tests/BaseTest.cs index 01013d845..18cdb6c94 100644 --- a/test/coverlet.integration.tests/BaseTest.cs +++ b/test/coverlet.integration.tests/BaseTest.cs @@ -15,221 +15,221 @@ namespace Coverlet.Integration.Tests { - [Flags] - public enum BuildConfiguration + [Flags] + public enum BuildConfiguration + { + Debug = 1, + Release = 2 + } + + public abstract class BaseTest + { + private static int s_folderSuffix; + + protected BuildConfiguration GetAssemblyBuildConfiguration() { - Debug = 1, - Release = 2 - } - - public abstract class BaseTest - { - private static int s_folderSuffix; - - protected BuildConfiguration GetAssemblyBuildConfiguration() - { #if DEBUG - return BuildConfiguration.Debug; + return BuildConfiguration.Debug; #endif #if RELEASE return BuildConfiguration.Release; #endif - throw new NotSupportedException($"Build configuration not supported"); - } + throw new NotSupportedException($"Build configuration not supported"); + } - private protected string GetPackageVersion(string filter) - { - string packagesPath = $"../../../../../bin/{GetAssemblyBuildConfiguration()}/Packages"; - - if (!Directory.Exists(packagesPath)) - { - throw new DirectoryNotFoundException("Package directory not found, run 'dotnet pack' on repository root"); - } - - var files = Directory.GetFiles(packagesPath, filter).ToList(); - if (files.Count == 0) - { - throw new InvalidOperationException($"Could not find any package using filter '{filter}' in folder '{Path.GetFullPath(packagesPath)}'. Make sure 'dotnet pack' was called."); - } - else if (files.Count > 1) - { - throw new InvalidOperationException($"Found more than one package using filter '{filter}' in folder '{Path.GetFullPath(packagesPath)}'. Make sure 'dotnet pack' was only called once."); - } - else - { - using Stream pkg = File.OpenRead(files[0]); - using var reader = new PackageArchiveReader(pkg); - using Stream nuspecStream = reader.GetNuspec(); - var manifest = Manifest.ReadFrom(nuspecStream, false); - return manifest.Metadata.Version.OriginalVersion; - } - } + private protected string GetPackageVersion(string filter) + { + string packagesPath = $"../../../../../bin/{GetAssemblyBuildConfiguration()}/Packages"; + + if (!Directory.Exists(packagesPath)) + { + throw new DirectoryNotFoundException("Package directory not found, run 'dotnet pack' on repository root"); + } + + var files = Directory.GetFiles(packagesPath, filter).ToList(); + if (files.Count == 0) + { + throw new InvalidOperationException($"Could not find any package using filter '{filter}' in folder '{Path.GetFullPath(packagesPath)}'. Make sure 'dotnet pack' was called."); + } + else if (files.Count > 1) + { + throw new InvalidOperationException($"Found more than one package using filter '{filter}' in folder '{Path.GetFullPath(packagesPath)}'. Make sure 'dotnet pack' was only called once."); + } + else + { + using Stream pkg = File.OpenRead(files[0]); + using var reader = new PackageArchiveReader(pkg); + using Stream nuspecStream = reader.GetNuspec(); + var manifest = Manifest.ReadFrom(nuspecStream, false); + return manifest.Metadata.Version.OriginalVersion; + } + } - private protected ClonedTemplateProject CloneTemplateProject(bool cleanupOnDispose = true, string testSDKVersion = "17.5.0") - { - DirectoryInfo finalRoot = Directory.CreateDirectory($"{Guid.NewGuid().ToString("N")[..6]}{Interlocked.Increment(ref s_folderSuffix)}"); - foreach (string file in (Directory.GetFiles($"../../../../coverlet.integration.template", "*.cs") - .Union(Directory.GetFiles($"../../../../coverlet.integration.template", "*.csproj") - .Union(Directory.GetFiles($"../../../../coverlet.integration.template", "nuget.config"))))) - { - File.Copy(file, Path.Combine(finalRoot.FullName, Path.GetFileName(file))); - } - - // We need to prevent the inheritance of global props/targets for template project - File.WriteAllText(Path.Combine(finalRoot.FullName, "Directory.Build.props"), + private protected ClonedTemplateProject CloneTemplateProject(bool cleanupOnDispose = true, string testSDKVersion = "17.5.0") + { + DirectoryInfo finalRoot = Directory.CreateDirectory($"{Guid.NewGuid().ToString("N")[..6]}{Interlocked.Increment(ref s_folderSuffix)}"); + foreach (string file in (Directory.GetFiles($"../../../../coverlet.integration.template", "*.cs") + .Union(Directory.GetFiles($"../../../../coverlet.integration.template", "*.csproj") + .Union(Directory.GetFiles($"../../../../coverlet.integration.template", "nuget.config"))))) + { + File.Copy(file, Path.Combine(finalRoot.FullName, Path.GetFileName(file))); + } + + // We need to prevent the inheritance of global props/targets for template project + File.WriteAllText(Path.Combine(finalRoot.FullName, "Directory.Build.props"), @" "); - File.WriteAllText(Path.Combine(finalRoot.FullName, "Directory.Build.targets"), + File.WriteAllText(Path.Combine(finalRoot.FullName, "Directory.Build.targets"), @" "); - AddMicrosoftNETTestSdkRef(finalRoot.FullName, testSDKVersion); + AddMicrosoftNETTestSdkRef(finalRoot.FullName, testSDKVersion); - SetIsTestProjectTrue(finalRoot.FullName); + SetIsTestProjectTrue(finalRoot.FullName); - return new ClonedTemplateProject(finalRoot.FullName, cleanupOnDispose); - } + return new ClonedTemplateProject(finalRoot.FullName, cleanupOnDispose); + } - private protected bool RunCommand(string command, string arguments, out string standardOutput, out string standardError, string workingDirectory = "") - { - Debug.WriteLine($"BaseTest.RunCommand: {command} {arguments}\nWorkingDirectory: {workingDirectory}"); - // https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardoutput?view=net-7.0&redirectedfrom=MSDN#System_Diagnostics_Process_StandardOutput - var commandProcess = new Process(); - commandProcess.StartInfo.FileName = command; - commandProcess.StartInfo.Arguments = arguments; - commandProcess.StartInfo.WorkingDirectory = workingDirectory; - commandProcess.StartInfo.RedirectStandardError = true; - commandProcess.StartInfo.RedirectStandardOutput = true; - commandProcess.StartInfo.UseShellExecute = false; - string eOut = ""; - commandProcess.ErrorDataReceived += new DataReceivedEventHandler((sender, e) => { eOut += e.Data; }); - commandProcess.Start(); - // To avoid deadlocks, use an asynchronous read operation on at least one of the streams. - commandProcess.BeginErrorReadLine(); - standardOutput = commandProcess.StandardOutput.ReadToEnd(); - if (!commandProcess.WaitForExit((int)TimeSpan.FromMinutes(5).TotalMilliseconds)) - { - throw new XunitException($"Command 'dotnet {arguments}' didn't end after 5 minute"); - } - standardError = eOut; - return commandProcess.ExitCode == 0; - } + private protected bool RunCommand(string command, string arguments, out string standardOutput, out string standardError, string workingDirectory = "") + { + Debug.WriteLine($"BaseTest.RunCommand: {command} {arguments}\nWorkingDirectory: {workingDirectory}"); + // https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardoutput?view=net-7.0&redirectedfrom=MSDN#System_Diagnostics_Process_StandardOutput + var commandProcess = new Process(); + commandProcess.StartInfo.FileName = command; + commandProcess.StartInfo.Arguments = arguments; + commandProcess.StartInfo.WorkingDirectory = workingDirectory; + commandProcess.StartInfo.RedirectStandardError = true; + commandProcess.StartInfo.RedirectStandardOutput = true; + commandProcess.StartInfo.UseShellExecute = false; + string eOut = ""; + commandProcess.ErrorDataReceived += new DataReceivedEventHandler((sender, e) => { eOut += e.Data; }); + commandProcess.Start(); + // To avoid deadlocks, use an asynchronous read operation on at least one of the streams. + commandProcess.BeginErrorReadLine(); + standardOutput = commandProcess.StandardOutput.ReadToEnd(); + if (!commandProcess.WaitForExit((int)TimeSpan.FromMinutes(5).TotalMilliseconds)) + { + throw new XunitException($"Command 'dotnet {arguments}' didn't end after 5 minute"); + } + standardError = eOut; + return commandProcess.ExitCode == 0; + } - private protected bool DotnetCli(string arguments, out string standardOutput, out string standardError, string workingDirectory = "") - { - return RunCommand("dotnet", arguments, out standardOutput, out standardError, workingDirectory); - } + private protected bool DotnetCli(string arguments, out string standardOutput, out string standardError, string workingDirectory = "") + { + return RunCommand("dotnet", arguments, out standardOutput, out standardError, workingDirectory); + } - private protected void UpdateNugeConfigtWithLocalPackageFolder(string projectPath) - { - string nugetFile = Path.Combine(projectPath, "nuget.config"); - if (!File.Exists(nugetFile)) - { - throw new FileNotFoundException("Nuget.config not found", "nuget.config"); - } - XDocument xml; - using (FileStream? nugetFileStream = File.OpenRead(nugetFile)) - { - xml = XDocument.Load(nugetFileStream); - } - - string localPackageFolder = Path.GetFullPath($"../../../../../bin/{GetAssemblyBuildConfiguration()}/Packages"); - xml.Element("configuration")! - .Element("packageSources")! - .Elements() - .ElementAt(0) - .AddAfterSelf(new XElement("add", new XAttribute("key", "localCoverletPackages"), new XAttribute("value", localPackageFolder))); - xml.Save(nugetFile); - } + private protected void UpdateNugeConfigtWithLocalPackageFolder(string projectPath) + { + string nugetFile = Path.Combine(projectPath, "nuget.config"); + if (!File.Exists(nugetFile)) + { + throw new FileNotFoundException("Nuget.config not found", "nuget.config"); + } + XDocument xml; + using (FileStream? nugetFileStream = File.OpenRead(nugetFile)) + { + xml = XDocument.Load(nugetFileStream); + } + + string localPackageFolder = Path.GetFullPath($"../../../../../bin/{GetAssemblyBuildConfiguration()}/Packages"); + xml.Element("configuration")! + .Element("packageSources")! + .Elements() + .ElementAt(0) + .AddAfterSelf(new XElement("add", new XAttribute("key", "localCoverletPackages"), new XAttribute("value", localPackageFolder))); + xml.Save(nugetFile); + } - private void SetIsTestProjectTrue(string projectPath) - { - string csproj = Path.Combine(projectPath, "coverlet.integration.template.csproj"); - if (!File.Exists(csproj)) - { - throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); - } - XDocument xml; - using (FileStream? csprojStream = File.OpenRead(csproj)) - { - xml = XDocument.Load(csprojStream); - } - - xml.Element("Project")! - .Element("PropertyGroup")! - .Element("IsTestProject")!.Value = "true"; - - xml.Save(csproj); - } + private void SetIsTestProjectTrue(string projectPath) + { + string csproj = Path.Combine(projectPath, "coverlet.integration.template.csproj"); + if (!File.Exists(csproj)) + { + throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); + } + XDocument xml; + using (FileStream? csprojStream = File.OpenRead(csproj)) + { + xml = XDocument.Load(csprojStream); + } + + xml.Element("Project")! + .Element("PropertyGroup")! + .Element("IsTestProject")!.Value = "true"; + + xml.Save(csproj); + } - private protected void AddMicrosoftNETTestSdkRef(string projectPath, string version) - { - string csproj = Path.Combine(projectPath, "coverlet.integration.template.csproj"); - if (!File.Exists(csproj)) - { - throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); - } - XDocument xml; - using (FileStream? csprojStream = File.OpenRead(csproj)) - { - xml = XDocument.Load(csprojStream); - } - - xml.Element("Project")! - .Element("ItemGroup")! - .Add(new XElement("PackageReference", new XAttribute("Include", "Microsoft.NET.Test.Sdk"), - new XAttribute("Version", version))); - xml.Save(csproj); - } + private protected void AddMicrosoftNETTestSdkRef(string projectPath, string version) + { + string csproj = Path.Combine(projectPath, "coverlet.integration.template.csproj"); + if (!File.Exists(csproj)) + { + throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); + } + XDocument xml; + using (FileStream? csprojStream = File.OpenRead(csproj)) + { + xml = XDocument.Load(csprojStream); + } + + xml.Element("Project")! + .Element("ItemGroup")! + .Add(new XElement("PackageReference", new XAttribute("Include", "Microsoft.NET.Test.Sdk"), + new XAttribute("Version", version))); + xml.Save(csproj); + } - private protected void AddCoverletMsbuildRef(string projectPath) - { - string csproj = Path.Combine(projectPath, "coverlet.integration.template.csproj"); - if (!File.Exists(csproj)) - { - throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); - } - XDocument xml; - using (FileStream? csprojStream = File.OpenRead(csproj)) - { - xml = XDocument.Load(csprojStream); - } - string msbuildPkgVersion = GetPackageVersion("*msbuild*.nupkg"); - xml.Element("Project")! - .Element("ItemGroup")! - .Add(new XElement("PackageReference", new XAttribute("Include", "coverlet.msbuild"), new XAttribute("Version", msbuildPkgVersion), - new XElement("PrivateAssets", "all"), - new XElement("IncludeAssets", "runtime; build; native; contentfiles; analyzers"))); - xml.Save(csproj); - } + private protected void AddCoverletMsbuildRef(string projectPath) + { + string csproj = Path.Combine(projectPath, "coverlet.integration.template.csproj"); + if (!File.Exists(csproj)) + { + throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); + } + XDocument xml; + using (FileStream? csprojStream = File.OpenRead(csproj)) + { + xml = XDocument.Load(csprojStream); + } + string msbuildPkgVersion = GetPackageVersion("*msbuild*.nupkg"); + xml.Element("Project")! + .Element("ItemGroup")! + .Add(new XElement("PackageReference", new XAttribute("Include", "coverlet.msbuild"), new XAttribute("Version", msbuildPkgVersion), + new XElement("PrivateAssets", "all"), + new XElement("IncludeAssets", "runtime; build; native; contentfiles; analyzers"))); + xml.Save(csproj); + } - private protected void AddCoverletCollectosRef(string projectPath) - { - string csproj = Path.Combine(projectPath, "coverlet.integration.template.csproj"); - if (!File.Exists(csproj)) - { - throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); - } - XDocument xml; - using (FileStream? csprojStream = File.OpenRead(csproj)) - { - xml = XDocument.Load(csprojStream); - } - string msbuildPkgVersion = GetPackageVersion("*collector*.nupkg"); - xml.Element("Project")! - .Element("ItemGroup")! - .Add(new XElement("PackageReference", new XAttribute("Include", "coverlet.collector"), new XAttribute("Version", msbuildPkgVersion), - new XElement("PrivateAssets", "all"), - new XElement("IncludeAssets", "runtime; build; native; contentfiles; analyzers"))); - xml.Save(csproj); - } + private protected void AddCoverletCollectosRef(string projectPath) + { + string csproj = Path.Combine(projectPath, "coverlet.integration.template.csproj"); + if (!File.Exists(csproj)) + { + throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); + } + XDocument xml; + using (FileStream? csprojStream = File.OpenRead(csproj)) + { + xml = XDocument.Load(csprojStream); + } + string msbuildPkgVersion = GetPackageVersion("*collector*.nupkg"); + xml.Element("Project")! + .Element("ItemGroup")! + .Add(new XElement("PackageReference", new XAttribute("Include", "coverlet.collector"), new XAttribute("Version", msbuildPkgVersion), + new XElement("PrivateAssets", "all"), + new XElement("IncludeAssets", "runtime; build; native; contentfiles; analyzers"))); + xml.Save(csproj); + } - private protected string AddCollectorRunsettingsFile(string projectPath, string includeFilter = "[coverletsamplelib.integration.template]*DeepThought", bool sourceLink = false, bool deterministicReport = false) - { - string runSettings = + private protected string AddCollectorRunsettingsFile(string projectPath, string includeFilter = "[coverletsamplelib.integration.template]*DeepThought", bool sourceLink = false, bool deterministicReport = false) + { + string runSettings = $@" @@ -248,150 +248,150 @@ private protected string AddCollectorRunsettingsFile(string projectPath, string "; - string runsettingsPath = Path.Combine(projectPath, "runSettings"); - File.WriteAllText(runsettingsPath, runSettings); - return runsettingsPath; - } + string runsettingsPath = Path.Combine(projectPath, "runSettings"); + File.WriteAllText(runsettingsPath, runSettings); + return runsettingsPath; + } - private protected void AssertCoverage(ClonedTemplateProject clonedTemplateProject, string filter = "coverage.json", string standardOutput = "") + private protected void AssertCoverage(ClonedTemplateProject clonedTemplateProject, string filter = "coverage.json", string standardOutput = "") + { + if (GetAssemblyBuildConfiguration() == BuildConfiguration.Debug) + { + bool coverageChecked = false; + foreach (string coverageFile in clonedTemplateProject.GetFiles(filter)) { - if (GetAssemblyBuildConfiguration() == BuildConfiguration.Debug) - { - bool coverageChecked = false; - foreach (string coverageFile in clonedTemplateProject.GetFiles(filter)) - { - Classes? document = JsonConvert.DeserializeObject(File.ReadAllText(coverageFile))?.Document("DeepThought.cs"); - if (document != null) - { - document.Class("Coverlet.Integration.Template.DeepThought") - .Method("System.Int32 Coverlet.Integration.Template.DeepThought::AnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything()") - .AssertLinesCovered((6, 1), (7, 1), (8, 1)); - coverageChecked = true; - } - } - - Assert.True(coverageChecked, $"Coverage check fail\n{standardOutput}"); - } + Classes? document = JsonConvert.DeserializeObject(File.ReadAllText(coverageFile))?.Document("DeepThought.cs"); + if (document != null) + { + document.Class("Coverlet.Integration.Template.DeepThought") + .Method("System.Int32 Coverlet.Integration.Template.DeepThought::AnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything()") + .AssertLinesCovered((6, 1), (7, 1), (8, 1)); + coverageChecked = true; + } } - private protected void UpdateProjectTargetFramework(ClonedTemplateProject project, params string[] targetFrameworks) - { - if (targetFrameworks is null || targetFrameworks.Length == 0) - { - throw new ArgumentException("Invalid targetFrameworks", nameof(targetFrameworks)); - } - - if (!File.Exists(project.ProjectFileNamePath)) - { - throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); - } - XDocument xml; - using (FileStream? csprojStream = File.OpenRead(project.ProjectFileNamePath)) - { - xml = XDocument.Load(csprojStream); - } - - xml.Element("Project")! - .Element("PropertyGroup")! - .Element("TargetFramework")! - .Remove(); - - XElement targetFrameworkElement; - - if (targetFrameworks.Length == 1) - { - targetFrameworkElement = new XElement("TargetFramework", targetFrameworks[0]); - } - else - { - targetFrameworkElement = new XElement("TargetFrameworks", string.Join(';', targetFrameworks)); - } - - xml.Element("Project")!.Element("PropertyGroup")!.Add(targetFrameworkElement); - xml.Save(project.ProjectFileNamePath); - } + Assert.True(coverageChecked, $"Coverage check fail\n{standardOutput}"); + } + } - private protected void PinSDK(ClonedTemplateProject project, string sdkVersion) - { - if (project is null) - { - throw new ArgumentNullException(nameof(project)); - } - - if (string.IsNullOrEmpty(sdkVersion)) - { - throw new ArgumentException("Invalid sdkVersion", nameof(sdkVersion)); - } - - if (!File.Exists(project.ProjectFileNamePath)) - { - throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); - } - - if (project.ProjectRootPath is null || !Directory.Exists(project.ProjectRootPath)) - { - throw new ArgumentException("Invalid ProjectRootPath"); - } - - File.WriteAllText(Path.Combine(project.ProjectRootPath, "global.json"), $"{{ \"sdk\": {{ \"version\": \"{sdkVersion}\" }} }}"); - } + private protected void UpdateProjectTargetFramework(ClonedTemplateProject project, params string[] targetFrameworks) + { + if (targetFrameworks is null || targetFrameworks.Length == 0) + { + throw new ArgumentException("Invalid targetFrameworks", nameof(targetFrameworks)); + } + + if (!File.Exists(project.ProjectFileNamePath)) + { + throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); + } + XDocument xml; + using (FileStream? csprojStream = File.OpenRead(project.ProjectFileNamePath)) + { + xml = XDocument.Load(csprojStream); + } + + xml.Element("Project")! + .Element("PropertyGroup")! + .Element("TargetFramework")! + .Remove(); + + XElement targetFrameworkElement; + + if (targetFrameworks.Length == 1) + { + targetFrameworkElement = new XElement("TargetFramework", targetFrameworks[0]); + } + else + { + targetFrameworkElement = new XElement("TargetFrameworks", string.Join(';', targetFrameworks)); + } + + xml.Element("Project")!.Element("PropertyGroup")!.Add(targetFrameworkElement); + xml.Save(project.ProjectFileNamePath); } - class ClonedTemplateProject : IDisposable + private protected void PinSDK(ClonedTemplateProject project, string sdkVersion) { - public string ProjectRootPath { get; private set; } - public bool CleanupOnDispose { get; private set; } + if (project is null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (string.IsNullOrEmpty(sdkVersion)) + { + throw new ArgumentException("Invalid sdkVersion", nameof(sdkVersion)); + } + + if (!File.Exists(project.ProjectFileNamePath)) + { + throw new FileNotFoundException("coverlet.integration.template.csproj not found", "coverlet.integration.template.csproj"); + } + + if (project.ProjectRootPath is null || !Directory.Exists(project.ProjectRootPath)) + { + throw new ArgumentException("Invalid ProjectRootPath"); + } + + File.WriteAllText(Path.Combine(project.ProjectRootPath, "global.json"), $"{{ \"sdk\": {{ \"version\": \"{sdkVersion}\" }} }}"); + } + } - // We need to have a different asm name to avoid issue with collectors, we filter [coverlet.*]* by default - // https://github.com/tonerdo/coverlet/pull/410#discussion_r284526728 - public static string AssemblyName { get; } = "coverletsamplelib.integration.template"; - public static string ProjectFileName { get; } = "coverlet.integration.template.csproj"; - public string ProjectFileNamePath => Path.Combine(ProjectRootPath, "coverlet.integration.template.csproj"); + class ClonedTemplateProject : IDisposable + { + public string ProjectRootPath { get; private set; } + public bool CleanupOnDispose { get; private set; } - public ClonedTemplateProject(string? projectRootPath, bool cleanupOnDispose) - { - ProjectRootPath = (projectRootPath ?? throw new ArgumentNullException(nameof(projectRootPath))); - CleanupOnDispose = cleanupOnDispose; - } + // We need to have a different asm name to avoid issue with collectors, we filter [coverlet.*]* by default + // https://github.com/tonerdo/coverlet/pull/410#discussion_r284526728 + public static string AssemblyName { get; } = "coverletsamplelib.integration.template"; + public static string ProjectFileName { get; } = "coverlet.integration.template.csproj"; + public string ProjectFileNamePath => Path.Combine(ProjectRootPath, "coverlet.integration.template.csproj"); - public bool IsMultipleTargetFramework() - { - using FileStream? csprojStream = File.OpenRead(ProjectFileNamePath); - var xml = XDocument.Load(csprojStream); - return xml.Element("Project")!.Element("PropertyGroup")!.Element("TargetFramework") == null; - } + public ClonedTemplateProject(string? projectRootPath, bool cleanupOnDispose) + { + ProjectRootPath = (projectRootPath ?? throw new ArgumentNullException(nameof(projectRootPath))); + CleanupOnDispose = cleanupOnDispose; + } - public string[] GetTargetFrameworks() - { - using FileStream? csprojStream = File.OpenRead(ProjectFileNamePath); - var xml = XDocument.Load(csprojStream); - XElement element = xml.Element("Project")!.Element("PropertyGroup")!.Element("TargetFramework") ?? xml.Element("Project")!.Element("PropertyGroup")!.Element("TargetFrameworks")!; - if (element is null) - { - throw new ArgumentNullException("No 'TargetFramework' neither 'TargetFrameworks' found in csproj file"); - } - return element.Value.Split(";"); - } + public bool IsMultipleTargetFramework() + { + using FileStream? csprojStream = File.OpenRead(ProjectFileNamePath); + var xml = XDocument.Load(csprojStream); + return xml.Element("Project")!.Element("PropertyGroup")!.Element("TargetFramework") == null; + } - public string[] GetFiles(string filter) + public string[] GetTargetFrameworks() + { + using FileStream? csprojStream = File.OpenRead(ProjectFileNamePath); + var xml = XDocument.Load(csprojStream); + XElement element = xml.Element("Project")!.Element("PropertyGroup")!.Element("TargetFramework") ?? xml.Element("Project")!.Element("PropertyGroup")!.Element("TargetFrameworks")!; + if (element is null) + { + throw new ArgumentNullException("No 'TargetFramework' neither 'TargetFrameworks' found in csproj file"); + } + return element.Value.Split(";"); + } + + public string[] GetFiles(string filter) + { + return Directory.GetFiles(ProjectRootPath, filter, SearchOption.AllDirectories); + } + + public void Dispose() + { + if (CleanupOnDispose) + { + try { - return Directory.GetFiles(ProjectRootPath, filter, SearchOption.AllDirectories); + // Directory.Delete(ProjectRootPath, true); } - - public void Dispose() + catch (UnauthorizedAccessException) { - if (CleanupOnDispose) - { - try - { - // Directory.Delete(ProjectRootPath, true); - } - catch (UnauthorizedAccessException) - { - // Sometimes on CI AzDo we get Access Denied on delete - // swallowed exception to not waste time - } - } + // Sometimes on CI AzDo we get Access Denied on delete + // swallowed exception to not waste time } + } } + } } diff --git a/test/coverlet.integration.tests/DeterministicBuild.cs b/test/coverlet.integration.tests/DeterministicBuild.cs index 910f5fcbe..1e6b378bf 100644 --- a/test/coverlet.integration.tests/DeterministicBuild.cs +++ b/test/coverlet.integration.tests/DeterministicBuild.cs @@ -40,39 +40,39 @@ private void CreateDeterministicTestPropsFile() deterministicTestProps.Save(Path.Combine(_testProjectPath, PropsFileName)); } - private protected void AssertCoverage(string standardOutput = "", bool checkDeterministicReport = true) + private protected void AssertCoverage(string standardOutput = "", bool checkDeterministicReport = true) + { + if (_buildConfiguration == "Debug") + { + bool coverageChecked = false; + string reportFilePath = ""; + foreach (string coverageFile in Directory.GetFiles(GetReportPath(standardOutput), "coverage.json", SearchOption.AllDirectories)) + { + Classes? document = JsonConvert.DeserializeObject(File.ReadAllText(coverageFile))?.Document("DeepThought.cs"); + if (document != null) + { + document.Class("Coverlet.Integration.DeterministicBuild.DeepThought") + .Method("System.Int32 Coverlet.Integration.DeterministicBuild.DeepThought::AnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything()") + .AssertLinesCovered((6, 1), (7, 1), (8, 1)); + coverageChecked = true; + reportFilePath = coverageFile; + } + } + Assert.True(coverageChecked, $"Coverage check fail\n{standardOutput}"); + File.Delete(reportFilePath); + Assert.False(File.Exists(reportFilePath)); + + if (checkDeterministicReport) { - if (_buildConfiguration == "Debug") - { - bool coverageChecked = false; - string reportFilePath = ""; - foreach (string coverageFile in Directory.GetFiles(GetReportPath(standardOutput), "coverage.json", SearchOption.AllDirectories)) - { - Classes? document = JsonConvert.DeserializeObject(File.ReadAllText(coverageFile))?.Document("DeepThought.cs"); - if (document != null) - { - document.Class("Coverlet.Integration.DeterministicBuild.DeepThought") - .Method("System.Int32 Coverlet.Integration.DeterministicBuild.DeepThought::AnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything()") - .AssertLinesCovered((6, 1), (7, 1), (8, 1)); - coverageChecked = true; - reportFilePath = coverageFile; - } - } - Assert.True(coverageChecked, $"Coverage check fail\n{standardOutput}"); - File.Delete(reportFilePath); - Assert.False(File.Exists(reportFilePath)); - - if (checkDeterministicReport) - { - // Verify deterministic report - foreach (string coverageFile in Directory.GetFiles(GetReportPath(standardOutput), "coverage.cobertura.xml", SearchOption.AllDirectories)) - { - Assert.Contains("/_/test/coverlet.integration.determisticbuild/DeepThought.cs", File.ReadAllText(coverageFile)); - File.Delete(coverageFile); - } - } - } + // Verify deterministic report + foreach (string coverageFile in Directory.GetFiles(GetReportPath(standardOutput), "coverage.cobertura.xml", SearchOption.AllDirectories)) + { + Assert.Contains("/_/test/coverlet.integration.determisticbuild/DeepThought.cs", File.ReadAllText(coverageFile)); + File.Delete(coverageFile); + } } + } + } [Fact] public void Msbuild() @@ -152,19 +152,19 @@ public void Collectors() Assert.NotEmpty(File.ReadAllText(sourceRootMappingFilePath)); Assert.Contains("=/_/", File.ReadAllText(sourceRootMappingFilePath), StringComparison.OrdinalIgnoreCase); - string runSettingsPath = AddCollectorRunsettingsFile(_testProjectPath, "[coverletsample.integration.determisticbuild]*DeepThought", deterministicReport: true); - bool result = DotnetCli($"test -c {_buildConfiguration} --no-build \"{_testProjectPath}\" --collect:\"XPlat Code Coverage\" --settings \"{runSettingsPath}\" --diag:{Path.Combine(_testProjectPath, "log.txt")}", out standardOutput, out standardError); - if (!string.IsNullOrEmpty(standardError)) - { - _output.WriteLine(standardError); - } - else - { - _output.WriteLine(standardOutput); - } - Assert.True(result); - Assert.Contains("Passed!", standardOutput); - AssertCoverage(standardOutput); + string runSettingsPath = AddCollectorRunsettingsFile(_testProjectPath, "[coverletsample.integration.determisticbuild]*DeepThought", deterministicReport: true); + bool result = DotnetCli($"test -c {_buildConfiguration} --no-build \"{_testProjectPath}\" --collect:\"XPlat Code Coverage\" --settings \"{runSettingsPath}\" --diag:{Path.Combine(_testProjectPath, "log.txt")}", out standardOutput, out standardError); + if (!string.IsNullOrEmpty(standardError)) + { + _output.WriteLine(standardError); + } + else + { + _output.WriteLine(standardOutput); + } + Assert.True(result); + Assert.Contains("Passed!", standardOutput); + AssertCoverage(standardOutput); // Check out/in process collectors injection string dataCollectorLogContent = File.ReadAllText(Directory.GetFiles(_testProjectPath, "log.datacollector.*.txt").Single()); @@ -178,8 +178,8 @@ public void Collectors() RunCommand("git", "clean -fdx", out _, out _, _testProjectPath); } - [Fact] - public void Collectors_SourceLink() + [Fact] + public void Collectors_SourceLink() { CreateDeterministicTestPropsFile(); DotnetCli($"build -c {_buildConfiguration} /p:DeterministicSourcePaths=true", out string standardOutput, out string standardError, _testProjectPath); @@ -231,8 +231,8 @@ private string GetReportPath(string standardOutput) } public void Dispose() - { - File.Delete(Path.Combine(_testProjectPath, PropsFileName)); - } + { + File.Delete(Path.Combine(_testProjectPath, PropsFileName)); } + } } diff --git a/test/coverlet.integration.tests/Properties/AssemblyInfo.cs b/test/coverlet.integration.tests/Properties/AssemblyInfo.cs index 73e98ccb6..af49d7698 100644 --- a/test/coverlet.integration.tests/Properties/AssemblyInfo.cs +++ b/test/coverlet.integration.tests/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Reflection; diff --git a/test/coverlet.integration.tests/WpfResolverTests.cs b/test/coverlet.integration.tests/WpfResolverTests.cs index 6ba05e1d8..2758c0bfc 100644 --- a/test/coverlet.integration.tests/WpfResolverTests.cs +++ b/test/coverlet.integration.tests/WpfResolverTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; @@ -13,32 +13,32 @@ namespace Coverlet.Integration.Tests { - public class WpfResolverTests : BaseTest + public class WpfResolverTests : BaseTest + { + [ConditionalFact] + [SkipOnOS(OS.Linux, "WPF only runs on Windows")] + [SkipOnOS(OS.MacOS, "WPF only runs on Windows")] + public void TestInstrument_NetCoreSharedFrameworkResolver() { - [ConditionalFact] - [SkipOnOS(OS.Linux, "WPF only runs on Windows")] - [SkipOnOS(OS.MacOS, "WPF only runs on Windows")] - public void TestInstrument_NetCoreSharedFrameworkResolver() - { - string wpfProjectPath = "../../../../coverlet.tests.projectsample.wpf6"; - Assert.True(DotnetCli($"build \"{wpfProjectPath}\"", out string output, out string error)); - string assemblyLocation = Directory.GetFiles($"{wpfProjectPath}/bin", "coverlet.tests.projectsample.wpf6.dll", SearchOption.AllDirectories).First(); + string wpfProjectPath = "../../../../coverlet.tests.projectsample.wpf6"; + Assert.True(DotnetCli($"build \"{wpfProjectPath}\"", out string output, out string error)); + string assemblyLocation = Directory.GetFiles($"{wpfProjectPath}/bin", "coverlet.tests.projectsample.wpf6.dll", SearchOption.AllDirectories).First(); - var mockLogger = new Mock(); - var resolver = new NetCoreSharedFrameworkResolver(assemblyLocation, mockLogger.Object); - var compilationLibrary = new CompilationLibrary( - "package", - "System.Drawing", - "0.0.0.0", - "sha512-not-relevant", - Enumerable.Empty(), - Enumerable.Empty(), - true); + var mockLogger = new Mock(); + var resolver = new NetCoreSharedFrameworkResolver(assemblyLocation, mockLogger.Object); + var compilationLibrary = new CompilationLibrary( + "package", + "System.Drawing", + "0.0.0.0", + "sha512-not-relevant", + Enumerable.Empty(), + Enumerable.Empty(), + true); - var assemblies = new List(); - Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies), - "sample assembly shall be resolved"); - Assert.NotEmpty(assemblies); - } + var assemblies = new List(); + Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies), + "sample assembly shall be resolved"); + Assert.NotEmpty(assemblies); } + } } diff --git a/test/coverlet.msbuild.tasks.tests/CoverageResultTaskTests.cs b/test/coverlet.msbuild.tasks.tests/CoverageResultTaskTests.cs index 5529f888e..319a10812 100644 --- a/test/coverlet.msbuild.tasks.tests/CoverageResultTaskTests.cs +++ b/test/coverlet.msbuild.tasks.tests/CoverageResultTaskTests.cs @@ -4,10 +4,10 @@ using Coverlet.Core.Abstractions; using Coverlet.Core.Helpers; using Coverlet.MSbuild.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Build.Framework; using Microsoft.Build.Locator; using Microsoft.Build.Utilities; +using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; @@ -87,7 +87,7 @@ public void Execute_StateUnderTest_WithInstrumentationState_Fake() #pragma warning disable CS8604 // Possible null reference argument for parameter.. #pragma warning disable CS8602 // Dereference of a possibly null reference. - var InstrumenterState = new TaskItem(Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "TestAssets\\InstrumenterState.ItemSpec.data1.xml")) ; + var InstrumenterState = new TaskItem(Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "TestAssets\\InstrumenterState.ItemSpec.data1.xml")); #pragma warning restore CS8602 // Dereference of a possibly null reference. #pragma warning restore C8S604 // Possible null reference argument for parameter. diff --git a/test/coverlet.tests.projectsample.aspmvcrazor.tests/ResolverTests.cs b/test/coverlet.tests.projectsample.aspmvcrazor.tests/ResolverTests.cs index dadf5a7b3..466572e04 100644 --- a/test/coverlet.tests.projectsample.aspmvcrazor.tests/ResolverTests.cs +++ b/test/coverlet.tests.projectsample.aspmvcrazor.tests/ResolverTests.cs @@ -12,27 +12,27 @@ namespace coverlet.tests.projectsample.aspmvcrazor.tests { - public class ResolverTests + public class ResolverTests + { + [Fact] + public void TestInstrument_NetCoreSharedFrameworkResolver() { - [Fact] - public void TestInstrument_NetCoreSharedFrameworkResolver() - { - Assembly assembly = GetType().Assembly; - var mockLogger = new Mock(); - var resolver = new NetCoreSharedFrameworkResolver(assembly.Location, mockLogger.Object); - var compilationLibrary = new CompilationLibrary( - "package", - "Microsoft.AspNetCore.Mvc.Razor", - "0.0.0.0", - "sha512-not-relevant", - Enumerable.Empty(), - Enumerable.Empty(), - true); + Assembly assembly = GetType().Assembly; + var mockLogger = new Mock(); + var resolver = new NetCoreSharedFrameworkResolver(assembly.Location, mockLogger.Object); + var compilationLibrary = new CompilationLibrary( + "package", + "Microsoft.AspNetCore.Mvc.Razor", + "0.0.0.0", + "sha512-not-relevant", + Enumerable.Empty(), + Enumerable.Empty(), + true); - var assemblies = new List(); - Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies), - "sample assembly shall be resolved"); - Assert.NotEmpty(assemblies); - } + var assemblies = new List(); + Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies), + "sample assembly shall be resolved"); + Assert.NotEmpty(assemblies); } + } } diff --git a/test/coverlet.tests.projectsample.aspmvcrazor/Class.cs b/test/coverlet.tests.projectsample.aspmvcrazor/Class.cs index 259b4655e..87d944ba8 100644 --- a/test/coverlet.tests.projectsample.aspmvcrazor/Class.cs +++ b/test/coverlet.tests.projectsample.aspmvcrazor/Class.cs @@ -5,12 +5,12 @@ namespace coverlet.tests.projectsample.aspmvcrazor { - public static class Class + public static class Class + { + public static IMvcBuilder AddLocalization(this IMvcBuilder mvcBuilder, + LanguageViewLocationExpanderFormat viewLocationExpanderFormat = LanguageViewLocationExpanderFormat.Suffix) { - public static IMvcBuilder AddLocalization(this IMvcBuilder mvcBuilder, - LanguageViewLocationExpanderFormat viewLocationExpanderFormat = LanguageViewLocationExpanderFormat.Suffix) - { - return mvcBuilder; - } + return mvcBuilder; } + } } diff --git a/test/coverlet.tests.projectsample.aspmvcrazor/Program.cs b/test/coverlet.tests.projectsample.aspmvcrazor/Program.cs index 12efc2fd4..4088806b3 100644 --- a/test/coverlet.tests.projectsample.aspmvcrazor/Program.cs +++ b/test/coverlet.tests.projectsample.aspmvcrazor/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. var builder = WebApplication.CreateBuilder(args); @@ -11,9 +11,9 @@ // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); } app.UseHttpsRedirection(); diff --git a/test/coverlet.tests.projectsample.aspnet6.tests/ResolverTests.cs b/test/coverlet.tests.projectsample.aspnet6.tests/ResolverTests.cs index 216f1a624..261b1b0ef 100644 --- a/test/coverlet.tests.projectsample.aspnet6.tests/ResolverTests.cs +++ b/test/coverlet.tests.projectsample.aspnet6.tests/ResolverTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; @@ -12,27 +12,27 @@ namespace coverlet.tests.projectsample.aspnet6.tests { - public class ResolverTests + public class ResolverTests + { + [Fact] + public void TestInstrument_NetCoreSharedFrameworkResolver() { - [Fact] - public void TestInstrument_NetCoreSharedFrameworkResolver() - { - Assembly assembly = GetType().Assembly; - var mockLogger = new Mock(); - var resolver = new NetCoreSharedFrameworkResolver(assembly.Location, mockLogger.Object); - var compilationLibrary = new CompilationLibrary( - "package", - "Microsoft.Extensions.Logging.Abstractions", - "0.0.0.0", - "sha512-not-relevant", - Enumerable.Empty(), - Enumerable.Empty(), - true); + Assembly assembly = GetType().Assembly; + var mockLogger = new Mock(); + var resolver = new NetCoreSharedFrameworkResolver(assembly.Location, mockLogger.Object); + var compilationLibrary = new CompilationLibrary( + "package", + "Microsoft.Extensions.Logging.Abstractions", + "0.0.0.0", + "sha512-not-relevant", + Enumerable.Empty(), + Enumerable.Empty(), + true); - var assemblies = new List(); - Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies), - "sample assembly shall be resolved"); - Assert.NotEmpty(assemblies); - } + var assemblies = new List(); + Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies), + "sample assembly shall be resolved"); + Assert.NotEmpty(assemblies); } + } } diff --git a/test/coverlet.tests.projectsample.aspnet6/Program.cs b/test/coverlet.tests.projectsample.aspnet6/Program.cs index dba32bba8..904bf5c22 100644 --- a/test/coverlet.tests.projectsample.aspnet6/Program.cs +++ b/test/coverlet.tests.projectsample.aspnet6/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.AspNetCore.Hosting; @@ -6,18 +6,18 @@ namespace coverlet.tests.projectsample.aspnet6 { - public class Program + public class Program + { + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } } diff --git a/test/coverlet.tests.projectsample.aspnet6/Startup.cs b/test/coverlet.tests.projectsample.aspnet6/Startup.cs index 7c22a4c60..6c9ade640 100644 --- a/test/coverlet.tests.projectsample.aspnet6/Startup.cs +++ b/test/coverlet.tests.projectsample.aspnet6/Startup.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.AspNetCore.Builder; @@ -9,31 +9,31 @@ namespace coverlet.tests.projectsample.aspnet6 { - public class Startup + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - } + } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } - app.UseRouting(); + app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapGet("/", async context => - { - await context.Response.WriteAsync("Hello World!"); - }); - }); - } + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", async context => + { + await context.Response.WriteAsync("Hello World!"); + }); + }); } + } } diff --git a/test/coverlet.tests.projectsample.vbmynamespace/SampleVbClass.vb b/test/coverlet.tests.projectsample.vbmynamespace/SampleVbClass.vb index bce7be710..8519beb3e 100644 --- a/test/coverlet.tests.projectsample.vbmynamespace/SampleVbClass.vb +++ b/test/coverlet.tests.projectsample.vbmynamespace/SampleVbClass.vb @@ -1,5 +1,5 @@ Public Class SampleVbClass - Sub SampleSub() - Return - End Sub + Sub SampleSub() + Return + End Sub End Class diff --git a/test/coverlet.tests.xunit.extensions/ConditionalFact.cs b/test/coverlet.tests.xunit.extensions/ConditionalFact.cs index fbcbc82f6..3f7f7798d 100644 --- a/test/coverlet.tests.xunit.extensions/ConditionalFact.cs +++ b/test/coverlet.tests.xunit.extensions/ConditionalFact.cs @@ -8,35 +8,35 @@ namespace Coverlet.Tests.Xunit.Extensions { - [AttributeUsage(AttributeTargets.Method)] - [XunitTestCaseDiscoverer("Coverlet.Tests.Xunit.Extensions." + nameof(ConditionalFactDiscoverer), "coverlet.tests.xunit.extensions")] - public class ConditionalFact : FactAttribute { } + [AttributeUsage(AttributeTargets.Method)] + [XunitTestCaseDiscoverer("Coverlet.Tests.Xunit.Extensions." + nameof(ConditionalFactDiscoverer), "coverlet.tests.xunit.extensions")] + public class ConditionalFact : FactAttribute { } - internal class ConditionalFactDiscoverer : FactDiscoverer - { - public ConditionalFactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } + internal class ConditionalFactDiscoverer : FactDiscoverer + { + public ConditionalFactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } - protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) - { - return new SkippableTestCase(testMethod.EvaluateSkipConditions(), DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod); - } + protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + return new SkippableTestCase(testMethod.EvaluateSkipConditions(), DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod); } + } - internal class SkippableTestCase : XunitTestCase - { - private readonly string _skipReason; + internal class SkippableTestCase : XunitTestCase + { + private readonly string _skipReason; - [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public SkippableTestCase() { } + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public SkippableTestCase() { } - public SkippableTestCase(string skipReason, IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[] testMethodArguments = null) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) - { - _skipReason = skipReason; - } - protected override string GetSkipReason(IAttributeInfo factAttribute) - { - return _skipReason ?? base.GetSkipReason(factAttribute); - } + public SkippableTestCase(string skipReason, IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[] testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) + { + _skipReason = skipReason; + } + protected override string GetSkipReason(IAttributeInfo factAttribute) + { + return _skipReason ?? base.GetSkipReason(factAttribute); } + } } diff --git a/test/coverlet.tests.xunit.extensions/Extensions.cs b/test/coverlet.tests.xunit.extensions/Extensions.cs index 2c3ee4a97..17a40b521 100644 --- a/test/coverlet.tests.xunit.extensions/Extensions.cs +++ b/test/coverlet.tests.xunit.extensions/Extensions.cs @@ -7,28 +7,28 @@ namespace Coverlet.Tests.Xunit.Extensions { - internal static class TestMethodExtensions + internal static class TestMethodExtensions + { + public static string EvaluateSkipConditions(this ITestMethod testMethod) { - public static string EvaluateSkipConditions(this ITestMethod testMethod) - { - ITypeInfo testClass = testMethod.TestClass.Class; - IAssemblyInfo assembly = testMethod.TestClass.TestCollection.TestAssembly.Assembly; - System.Collections.Generic.IEnumerable conditionAttributes = testMethod.Method - .GetCustomAttributes(typeof(ITestCondition)) - .Concat(testClass.GetCustomAttributes(typeof(ITestCondition))) - .Concat(assembly.GetCustomAttributes(typeof(ITestCondition))) - .OfType() - .Select(attributeInfo => attributeInfo.Attribute); - - foreach (ITestCondition condition in conditionAttributes) - { - if (!condition.IsMet) - { - return condition.SkipReason; - } - } + ITypeInfo testClass = testMethod.TestClass.Class; + IAssemblyInfo assembly = testMethod.TestClass.TestCollection.TestAssembly.Assembly; + System.Collections.Generic.IEnumerable conditionAttributes = testMethod.Method + .GetCustomAttributes(typeof(ITestCondition)) + .Concat(testClass.GetCustomAttributes(typeof(ITestCondition))) + .Concat(assembly.GetCustomAttributes(typeof(ITestCondition))) + .OfType() + .Select(attributeInfo => attributeInfo.Attribute); - return null; + foreach (ITestCondition condition in conditionAttributes) + { + if (!condition.IsMet) + { + return condition.SkipReason; } + } + + return null; } + } } diff --git a/test/coverlet.tests.xunit.extensions/ITestCondition.cs b/test/coverlet.tests.xunit.extensions/ITestCondition.cs index 874ca7b63..879341601 100644 --- a/test/coverlet.tests.xunit.extensions/ITestCondition.cs +++ b/test/coverlet.tests.xunit.extensions/ITestCondition.cs @@ -3,9 +3,9 @@ namespace Coverlet.Tests.Xunit.Extensions { - public interface ITestCondition - { - bool IsMet { get; } - string SkipReason { get; } - } + public interface ITestCondition + { + bool IsMet { get; } + string SkipReason { get; } + } } diff --git a/test/coverlet.tests.xunit.extensions/Properties/AssemblyInfo.cs b/test/coverlet.tests.xunit.extensions/Properties/AssemblyInfo.cs index 3e03952d4..66d31f1b9 100644 --- a/test/coverlet.tests.xunit.extensions/Properties/AssemblyInfo.cs +++ b/test/coverlet.tests.xunit.extensions/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Toni Solarin-Sodara +// Copyright (c) Toni Solarin-Sodara // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Reflection; diff --git a/test/coverlet.tests.xunit.extensions/SkipOnOS.cs b/test/coverlet.tests.xunit.extensions/SkipOnOS.cs index 9463171fb..a5e3af4cc 100644 --- a/test/coverlet.tests.xunit.extensions/SkipOnOS.cs +++ b/test/coverlet.tests.xunit.extensions/SkipOnOS.cs @@ -6,29 +6,29 @@ namespace Coverlet.Tests.Xunit.Extensions { - public enum OS - { - Linux, - MacOS, - Windows - } + public enum OS + { + Linux, + MacOS, + Windows + } - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)] - public class SkipOnOSAttribute : Attribute, ITestCondition - { - private readonly OS _os; - private readonly string _reason; + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)] + public class SkipOnOSAttribute : Attribute, ITestCondition + { + private readonly OS _os; + private readonly string _reason; - public SkipOnOSAttribute(OS os, string reason = "") => (_os, _reason) = (os, reason); + public SkipOnOSAttribute(OS os, string reason = "") => (_os, _reason) = (os, reason); - public bool IsMet => _os switch - { - OS.Linux => !RuntimeInformation.IsOSPlatform(OSPlatform.Linux), - OS.MacOS => !RuntimeInformation.IsOSPlatform(OSPlatform.OSX), - OS.Windows => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows), - _ => throw new NotSupportedException($"Not supported OS {_os}") - }; + public bool IsMet => _os switch + { + OS.Linux => !RuntimeInformation.IsOSPlatform(OSPlatform.Linux), + OS.MacOS => !RuntimeInformation.IsOSPlatform(OSPlatform.OSX), + OS.Windows => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows), + _ => throw new NotSupportedException($"Not supported OS {_os}") + }; - public string SkipReason => $"OS not supported{(string.IsNullOrEmpty(_reason) ? "" : $", {_reason}")}"; - } + public string SkipReason => $"OS not supported{(string.IsNullOrEmpty(_reason) ? "" : $", {_reason}")}"; + } }