diff --git a/.gitignore b/.gitignore index 56a4db2e5..7a005aa76 100644 --- a/.gitignore +++ b/.gitignore @@ -298,3 +298,6 @@ __pycache__/ # OSX .DS_Store + +# DeterministicSourcePaths workaround generated file DeterministicBuild.targets +.AssemblyAttributes diff --git a/DeterministicBuild.targets b/DeterministicBuild.targets new file mode 100644 index 000000000..78052937d --- /dev/null +++ b/DeterministicBuild.targets @@ -0,0 +1,23 @@ + + + + + $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) + + + + + + + + + + + <_LocalTopLevelSourceRoot Include="@(SourceRoot)" Condition="'%(SourceRoot.NestedRoot)' == ''"/> + + + diff --git a/Directory.Build.props b/Directory.Build.props index 686a97357..606e016b0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,6 +15,11 @@ + + true + true + + diff --git a/Directory.Build.targets b/Directory.Build.targets index 4c3816608..94f23698a 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -26,4 +26,7 @@ + + + diff --git a/coverlet.sln b/coverlet.sln index 57c594c15..4ec7ed78e 100644 --- a/coverlet.sln +++ b/coverlet.sln @@ -31,6 +31,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution eng\azure-pipelines-nightly.yml = eng\azure-pipelines-nightly.yml eng\azure-pipelines.yml = eng\azure-pipelines.yml eng\build.yml = eng\build.yml + DeterministicBuild.targets = DeterministicBuild.targets Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets global.json = global.json @@ -47,6 +48,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.core.tests.samples EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "coverlet.tests.xunit.extensions", "test\coverlet.tests.xunit.extensions\coverlet.tests.xunit.extensions.csproj", "{F8199E19-FA9A-4559-9101-CAD7028121B4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{9A8B19D4-4A24-4217-AEFE-159B68F029A1}" + ProjectSection(SolutionItems) = preProject + test\Directory.Build.props = test\Directory.Build.props + test\Directory.Build.targets = test\Directory.Build.targets + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -128,6 +135,7 @@ Global {F6FE7678-C662-43D3-AC6A-64F6AC5A5935} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} {5FF404AD-7C0B-465A-A1E9-558CDC642B0C} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} {F8199E19-FA9A-4559-9101-CAD7028121B4} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} + {9A8B19D4-4A24-4217-AEFE-159B68F029A1} = {2FEBDE1B-83E3-445B-B9F8-5644B0E0E134} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9CA57C02-97B0-4C38-A027-EA61E8741F10} diff --git a/eng/build.yml b/eng/build.yml index 5aff4fdb3..077400f31 100644 --- a/eng/build.yml +++ b/eng/build.yml @@ -6,7 +6,7 @@ steps: - task: UseDotNet@2 inputs: - version: 3.1.200 + version: 3.1.201 displayName: Install .NET Core SDK - script: dotnet restore diff --git a/global.json b/global.json index a90a85c6d..05d0ae3d2 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "3.1.*" + "version": "3.1.201" } } diff --git a/src/coverlet.collector/DataCollection/CoverageManager.cs b/src/coverlet.collector/DataCollection/CoverageManager.cs index 5dd59e2f6..d5330ddba 100644 --- a/src/coverlet.collector/DataCollection/CoverageManager.cs +++ b/src/coverlet.collector/DataCollection/CoverageManager.cs @@ -22,7 +22,8 @@ internal class CoverageManager public IReporter[] Reporters { get; } - public CoverageManager(CoverletSettings settings, TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, ICoverageWrapper coverageWrapper) + public CoverageManager(CoverletSettings settings, TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, ICoverageWrapper coverageWrapper, + IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator) : this(settings, settings.ReportFormats.Select(format => { @@ -38,18 +39,19 @@ public CoverageManager(CoverletSettings settings, TestPlatformEqtTrace eqtTrace, } }).Where(r => r != null).ToArray(), new CoverletLogger(eqtTrace, logger), - coverageWrapper) + coverageWrapper, instrumentationHelper, fileSystem, sourceRootTranslator) { } - public CoverageManager(CoverletSettings settings, IReporter[] reporters, ILogger logger, ICoverageWrapper coverageWrapper) + public CoverageManager(CoverletSettings settings, IReporter[] reporters, ILogger logger, ICoverageWrapper coverageWrapper, + IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator) { // Store input vars Reporters = reporters; _coverageWrapper = coverageWrapper; // Coverage object - _coverage = _coverageWrapper.CreateCoverage(settings, logger); + _coverage = _coverageWrapper.CreateCoverage(settings, logger, instrumentationHelper, fileSystem, sourceRootTranslator); } /// diff --git a/src/coverlet.collector/DataCollection/CoverageWrapper.cs b/src/coverlet.collector/DataCollection/CoverageWrapper.cs index 990274af3..27a0fb44a 100644 --- a/src/coverlet.collector/DataCollection/CoverageWrapper.cs +++ b/src/coverlet.collector/DataCollection/CoverageWrapper.cs @@ -16,7 +16,7 @@ internal class CoverageWrapper : ICoverageWrapper /// Coverlet settings /// Coverlet logger /// Coverage object - public Coverage CreateCoverage(CoverletSettings settings, ILogger coverletLogger) + public Coverage CreateCoverage(CoverletSettings settings, ILogger coverletLogger, IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator) { return new Coverage( settings.TestModule, @@ -30,8 +30,9 @@ public Coverage CreateCoverage(CoverletSettings settings, ILogger coverletLogger settings.MergeWith, settings.UseSourceLink, coverletLogger, - DependencyInjection.Current.GetService(), - DependencyInjection.Current.GetService()); + instrumentationHelper, + fileSystem, + sourceRootTranslator); } /// diff --git a/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs b/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs index 8d5e27638..75bea49ff 100644 --- a/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs +++ b/src/coverlet.collector/DataCollection/CoverletCoverageCollector.cs @@ -21,24 +21,28 @@ namespace Coverlet.Collector.DataCollection 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 ICoverageWrapper _coverageWrapper; - private ICountDownEventFactory _countDownEventFactory; + private IServiceProvider _serviceProvider; - public CoverletCoverageCollector() : this(new TestPlatformEqtTrace(), new CoverageWrapper(), new CollectorCountdownEventFactory()) + public CoverletCoverageCollector() : this(new TestPlatformEqtTrace(), new CoverageWrapper(), new CollectorCountdownEventFactory(), GetDefaultServiceCollection) { } - internal CoverletCoverageCollector(TestPlatformEqtTrace eqtTrace, ICoverageWrapper coverageWrapper, ICountDownEventFactory countDownEventFactory) : base() + internal CoverletCoverageCollector(TestPlatformEqtTrace eqtTrace, ICoverageWrapper coverageWrapper, ICountDownEventFactory countDownEventFactory, Func serviceCollectionFactory) : base() { _eqtTrace = eqtTrace; _coverageWrapper = coverageWrapper; _countDownEventFactory = countDownEventFactory; + _serviceCollectionFactory = serviceCollectionFactory; } private void AttachDebugger() @@ -79,7 +83,6 @@ public override void Initialize( _dataSink = dataSink; _dataCollectionContext = environmentContext.SessionDataCollectionContext; _logger = new TestPlatformLogger(logger, _dataCollectionContext); - DependencyInjection.Set(GetServiceProvider(_eqtTrace, _logger)); // Register events _events.SessionStart += OnSessionStart; @@ -125,8 +128,13 @@ private void OnSessionStart(object sender, SessionStartEventArgs sessionStartEve 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); + _coverageManager = new CoverageManager(coverletSettings, _eqtTrace, _logger, _coverageWrapper, + _serviceProvider.GetRequiredService(), _serviceProvider.GetRequiredService(), + _serviceProvider.GetRequiredService()); // Instrument modules _coverageManager.InstrumentModules(); @@ -209,18 +217,18 @@ private static IEnumerable GetPropertyValueWrapper(SessionStartEventArgs return sessionStartEventArgs.GetPropertyValue>(CoverletConstants.TestSourcesPropertyName); } - private static IServiceProvider GetServiceProvider(TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger) + private static IServiceCollection GetDefaultServiceCollection(TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) { IServiceCollection serviceCollection = new ServiceCollection(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(_ => new CoverletLogger(eqtTrace, logger)); - // We need to keep singleton/static semantics serviceCollection.AddSingleton(); - - return serviceCollection.BuildServiceProvider(); + // We cache resolutions + serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService())); + return serviceCollection; } } } diff --git a/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs b/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs index 59cab7c04..cb3340fe0 100644 --- a/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs +++ b/src/coverlet.collector/Utilities/Interfaces/ICoverageWrapper.cs @@ -15,8 +15,11 @@ internal interface ICoverageWrapper /// /// Coverlet settings /// Coverlet logger + /// Coverlet instrumentationHelper + /// Coverlet fileSystem + /// Coverlet sourceRootTranslator /// Coverage object - Coverage CreateCoverage(CoverletSettings settings, ILogger logger); + Coverage CreateCoverage(CoverletSettings settings, ILogger logger, IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem, ISourceRootTranslator sourceRootTranslator); /// /// Gets the coverage result from provided coverage object diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index 5670f2900..3855b06aa 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -26,14 +26,15 @@ static int Main(string[] args) serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); - // We need to keep singleton/static semantics serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService())); DependencyInjection.Set(serviceCollection.BuildServiceProvider()); - var logger = (ConsoleLogger) DependencyInjection.Current.GetService(); + var logger = (ConsoleLogger)DependencyInjection.Current.GetService(); var fileSystem = DependencyInjection.Current.GetService(); + var sourceTranslator = DependencyInjection.Current.GetService(); var app = new CommandLineApplication(); app.Name = "coverlet"; @@ -86,7 +87,8 @@ static int Main(string[] args) useSourceLink.HasValue(), logger, DependencyInjection.Current.GetService(), - fileSystem); + fileSystem, + sourceTranslator); coverage.PrepareModules(); Process process = new Process(); diff --git a/src/coverlet.core/Abstracts/IFileSystem.cs b/src/coverlet.core/Abstracts/IFileSystem.cs index c58b4fc79..6499e44ce 100644 --- a/src/coverlet.core/Abstracts/IFileSystem.cs +++ b/src/coverlet.core/Abstracts/IFileSystem.cs @@ -19,5 +19,7 @@ internal interface IFileSystem Stream NewFileStream(string path, FileMode mode); Stream NewFileStream(string path, FileMode mode, FileAccess access); + + string[] ReadAllLines(string path); } } diff --git a/src/coverlet.core/Abstracts/ISourceRootTranslator.cs b/src/coverlet.core/Abstracts/ISourceRootTranslator.cs new file mode 100644 index 000000000..b440acea1 --- /dev/null +++ b/src/coverlet.core/Abstracts/ISourceRootTranslator.cs @@ -0,0 +1,7 @@ +namespace Coverlet.Core.Abstracts +{ + internal interface ISourceRootTranslator + { + string ResolveFilePath(string originalFileName); + } +} diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index 1a2050824..2149b3321 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -26,6 +26,7 @@ internal class Coverage private ILogger _logger; private IInstrumentationHelper _instrumentationHelper; private IFileSystem _fileSystem; + private ISourceRootTranslator _sourceRootTranslator; private List _results; public string Identifier @@ -45,7 +46,8 @@ public Coverage(string module, bool useSourceLink, ILogger logger, IInstrumentationHelper instrumentationHelper, - IFileSystem fileSystem) + IFileSystem fileSystem, + ISourceRootTranslator sourceRootTranslator) { _module = module; _includeFilters = includeFilters; @@ -60,6 +62,7 @@ public Coverage(string module, _logger = logger; _instrumentationHelper = instrumentationHelper; _fileSystem = fileSystem; + _sourceRootTranslator = sourceRootTranslator; _identifier = Guid.NewGuid().ToString(); _results = new List(); @@ -97,7 +100,8 @@ public CoveragePrepareResult PrepareModules() continue; } - var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, _excludedSourceFiles, _excludeAttributes, _singleHit, _logger, _instrumentationHelper, _fileSystem); + var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, _excludedSourceFiles, _excludeAttributes, _singleHit, _logger, _instrumentationHelper, _fileSystem, _sourceRootTranslator); + if (instrumenter.CanInstrument()) { _instrumentationHelper.BackupOriginalModule(module, _identifier); diff --git a/src/coverlet.core/Helpers/FileSystem.cs b/src/coverlet.core/Helpers/FileSystem.cs index 7037214a6..45c8ce68a 100644 --- a/src/coverlet.core/Helpers/FileSystem.cs +++ b/src/coverlet.core/Helpers/FileSystem.cs @@ -48,5 +48,10 @@ public virtual Stream NewFileStream(string path, FileMode mode, FileAccess acces { return new FileStream(path, mode, access); } + + public string[] ReadAllLines(string path) + { + return File.ReadAllLines(path); + } } } diff --git a/src/coverlet.core/Helpers/InstrumentationHelper.cs b/src/coverlet.core/Helpers/InstrumentationHelper.cs index 7a586ad6a..20ff8073b 100644 --- a/src/coverlet.core/Helpers/InstrumentationHelper.cs +++ b/src/coverlet.core/Helpers/InstrumentationHelper.cs @@ -18,14 +18,16 @@ internal class InstrumentationHelper : IInstrumentationHelper private readonly ConcurrentDictionary _backupList = new ConcurrentDictionary(); 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) + 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; } public string[] GetCoverableModules(string module, string[] directories, bool includeTestAssembly) @@ -83,14 +85,14 @@ public bool HasPdb(string module, out bool embedded) if (entry.Type == DebugDirectoryEntryType.CodeView) { var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); - if (codeViewData.Path == $"{Path.GetFileNameWithoutExtension(module)}.pdb") + if (_sourceRootTranslator.ResolveFilePath(codeViewData.Path) == $"{Path.GetFileNameWithoutExtension(module)}.pdb") { // PDB is embedded embedded = true; return true; } - return _fileSystem.Exists(codeViewData.Path); + return _fileSystem.Exists(_sourceRootTranslator.ResolveFilePath(codeViewData.Path)); } } @@ -114,7 +116,7 @@ public bool EmbeddedPortablePdbHasLocalSource(string module, out string firstNot foreach (DocumentHandle docHandle in metadataReader.Documents) { Document document = metadataReader.GetDocument(docHandle); - string docName = metadataReader.GetString(document.Name); + string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name)); // We verify all docs and return false if not all are present in local // We could have false negative if doc is not a source @@ -146,7 +148,7 @@ public bool PortablePdbHasLocalSource(string module, out string firstNotFoundDoc if (entry.Type == DebugDirectoryEntryType.CodeView) { var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); - using Stream pdbStream = _fileSystem.OpenRead(codeViewData.Path); + using Stream pdbStream = _fileSystem.OpenRead(_sourceRootTranslator.ResolveFilePath(codeViewData.Path)); using MetadataReaderProvider metadataReaderProvider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); MetadataReader metadataReader = null; try @@ -161,7 +163,7 @@ public bool PortablePdbHasLocalSource(string module, out string firstNotFoundDoc foreach (DocumentHandle docHandle in metadataReader.Documents) { Document document = metadataReader.GetDocument(docHandle); - string docName = metadataReader.GetString(document.Name); + string docName = _sourceRootTranslator.ResolveFilePath(metadataReader.GetString(document.Name)); // We verify all docs and return false if not all are present in local // We could have false negative if doc is not a source diff --git a/src/coverlet.core/Helpers/SourceRootTranslator.cs b/src/coverlet.core/Helpers/SourceRootTranslator.cs new file mode 100644 index 000000000..be7604bce --- /dev/null +++ b/src/coverlet.core/Helpers/SourceRootTranslator.cs @@ -0,0 +1,105 @@ +using Coverlet.Core.Abstracts; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace Coverlet.Core.Helpers +{ + [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 const string MappingFileName = "CoverletSourceRootsMapping"; + private Dictionary _resolutionCache; + + public SourceRootTranslator(ILogger logger, IFileSystem fileSystem) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _sourceRootMapping = new Dictionary>(); + } + + public SourceRootTranslator(string moduleTestPath, ILogger logger, IFileSystem fileSystem) + { + _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 not found", moduleTestPath); + } + _sourceRootMapping = LoadSourceRootMapping(Path.GetDirectoryName(moduleTestPath)) ?? new Dictionary>(); + } + + private Dictionary> LoadSourceRootMapping(string directory) + { + Dictionary> mapping = new Dictionary>(); + + string mappingFilePath = Path.Combine(directory, MappingFileName); + if (!_fileSystem.Exists(mappingFilePath)) + { + return mapping; + } + + foreach (string mappingRecord in _fileSystem.ReadAllLines(mappingFilePath)) + { + int projecFileSeparatorIndex = mappingRecord.IndexOf('|'); + int pathMappingSeparatorIndex = mappingRecord.IndexOf('='); + if (projecFileSeparatorIndex == -1 || pathMappingSeparatorIndex == -1) + { + _logger.LogWarning($"Malformed mapping '{mappingRecord}'"); + continue; + } + string projectPath = mappingRecord.Substring(0, projecFileSeparatorIndex); + string originalPath = mappingRecord.Substring(projecFileSeparatorIndex + 1, pathMappingSeparatorIndex - projecFileSeparatorIndex - 1); + string mappedPath = mappingRecord.Substring(pathMappingSeparatorIndex + 1); + + if (!mapping.ContainsKey(mappedPath)) + { + mapping.Add(mappedPath, new List()); + } + mapping[mappedPath].Add(new SourceRootMapping() { OriginalPath = originalPath, ProjectPath = projectPath }); + } + + return mapping; + } + + public string ResolveFilePath(string originalFileName) + { + if (_resolutionCache != null && _resolutionCache.ContainsKey(originalFileName)) + { + return _resolutionCache[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)))) + { + (_resolutionCache ??= new Dictionary()).Add(originalFileName, pathToCheck); + _logger.LogVerbose($"Mapping resolved: '{originalFileName}' -> '{pathToCheck}'"); + return pathToCheck; + } + } + } + } + return originalFileName; + } + } +} diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index 4b84edd8b..fa26c75fc 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -29,6 +29,7 @@ internal class Instrumenter private readonly ILogger _logger; private readonly IInstrumentationHelper _instrumentationHelper; private readonly IFileSystem _fileSystem; + private readonly ISourceRootTranslator _sourceRootTranslator; private InstrumenterResult _result; private FieldDefinition _customTrackerHitsArray; private FieldDefinition _customTrackerHitsFilePath; @@ -54,7 +55,8 @@ public Instrumenter( bool singleHit, ILogger logger, IInstrumentationHelper instrumentationHelper, - IFileSystem fileSystem) + IFileSystem fileSystem, + ISourceRootTranslator sourceRootTranslator) { _module = module; _identifier = identifier; @@ -67,6 +69,7 @@ public Instrumenter( _logger = logger; _instrumentationHelper = instrumentationHelper; _fileSystem = fileSystem; + _sourceRootTranslator = sourceRootTranslator; } public bool CanInstrument() @@ -411,7 +414,7 @@ private void InstrumentType(TypeDefinition type) private void InstrumentMethod(MethodDefinition method) { - var sourceFile = method.DebugInformation.SequencePoints.Select(s => s.Document.Url).FirstOrDefault(); + var 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)) @@ -495,9 +498,9 @@ private void InstrumentIL(MethodDefinition method) private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, SequencePoint sequencePoint) { - if (!_result.Documents.TryGetValue(sequencePoint.Document.Url, out var document)) + if (!_result.Documents.TryGetValue(_sourceRootTranslator.ResolveFilePath(sequencePoint.Document.Url), out var document)) { - document = new Document { Path = sequencePoint.Document.Url }; + document = new Document { Path = _sourceRootTranslator.ResolveFilePath(sequencePoint.Document.Url) }; document.Index = _result.Documents.Count; _result.Documents.Add(document.Path, document); } @@ -515,9 +518,9 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor processor, Instruction instruction, BranchPoint branchPoint) { - if (!_result.Documents.TryGetValue(branchPoint.Document, out var document)) + if (!_result.Documents.TryGetValue(_sourceRootTranslator.ResolveFilePath(branchPoint.Document), out var document)) { - document = new Document { Path = branchPoint.Document }; + document = new Document { Path = _sourceRootTranslator.ResolveFilePath(branchPoint.Document) }; document.Index = _result.Documents.Count; _result.Documents.Add(document.Path, document); } diff --git a/src/coverlet.msbuild.tasks/InstrumentationTask.cs b/src/coverlet.msbuild.tasks/InstrumentationTask.cs index 3dbfc1b8d..4c6e15a57 100644 --- a/src/coverlet.msbuild.tasks/InstrumentationTask.cs +++ b/src/coverlet.msbuild.tasks/InstrumentationTask.cs @@ -122,12 +122,13 @@ public override bool Execute() WaitForDebuggerIfEnabled(); IServiceCollection serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); - serviceCollection.AddTransient(x => _logger); - + serviceCollection.AddTransient(_ => _logger); + serviceCollection.AddTransient(); + // We cache resolutions + serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(_path, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService())); // We need to keep singleton/static semantics serviceCollection.AddSingleton(); @@ -154,7 +155,8 @@ public override bool Execute() _useSourceLink, _logger, DependencyInjection.Current.GetService(), - fileSystem); + fileSystem, + DependencyInjection.Current.GetService()); CoveragePrepareResult prepareResult = coverage.PrepareModules(); InstrumenterState = new TaskItem(System.IO.Path.GetTempFileName()); diff --git a/src/coverlet.msbuild.tasks/coverlet.msbuild.targets b/src/coverlet.msbuild.tasks/coverlet.msbuild.targets index eecec4ee4..99d0eebab 100644 --- a/src/coverlet.msbuild.tasks/coverlet.msbuild.targets +++ b/src/coverlet.msbuild.tasks/coverlet.msbuild.targets @@ -3,6 +3,28 @@ + + + + + + <_byProject Include="@(_LocalTopLevelSourceRoot->'%(MSBuildSourceProjectFile)')" OriginalPath="%(Identity)" /> + <_mapping Include="@(_byProject->'%(Identity)|%(OriginalPath)=%(MappedPath)')" /> + + + <_sourceRootMappingFilePath>$(OutputPath)CoverletSourceRootsMapping + + + + + + + (); _mockCountDownEventFactory = new Mock(); + _mockCountDownEventFactory.Setup(def => def.Create(It.IsAny(), It.IsAny())).Returns(new Mock().Object); } [Fact] public void OnSessionStartShouldInitializeCoverageWithCorrectCoverletSettings() { - _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object); + Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => + { + IServiceCollection serviceCollection = new ServiceCollection(); + Mock 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())); + return serviceCollection; + }; + _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object, serviceCollectionFactory); _coverletCoverageDataCollector.Initialize( _configurationElement, _mockDataColectionEvents.Object, @@ -60,13 +76,27 @@ public void OnSessionStartShouldInitializeCoverageWithCorrectCoverletSettings() _mockDataColectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); - _mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is(y => string.Equals(y.TestModule, "abc.dll")), It.IsAny()), Times.Once); + _mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is(y => string.Equals(y.TestModule, "abc.dll")), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } [Fact] public void OnSessionStartShouldPrepareModulesForCoverage() { - _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object); + Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => + { + IServiceCollection serviceCollection = new ServiceCollection(); + Mock 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())); + return serviceCollection; + }; + _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object, serviceCollectionFactory); _coverletCoverageDataCollector.Initialize( _configurationElement, _mockDataColectionEvents.Object, @@ -74,21 +104,39 @@ public void OnSessionStartShouldPrepareModulesForCoverage() null, _context); IDictionary sessionStartProperties = new Dictionary(); - Coverage coverage = new Coverage("abc.dll", null, null, null, null, null, true, true, "abc.json", true, It.IsAny(), DependencyInjection.Current.GetService(), DependencyInjection.Current.GetService()); + IInstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + new Mock().Object); + + Coverage coverage = new Coverage("abc.dll", null, null, null, null, null, true, true, "abc.json", true, It.IsAny(), instrumentationHelper, new Mock().Object, new Mock().Object); sessionStartProperties.Add("TestSources", new List { "abc.dll" }); - _mockCoverageWrapper.Setup(x => x.CreateCoverage(It.IsAny(), It.IsAny())).Returns(coverage); + _mockCoverageWrapper.Setup(x => x.CreateCoverage(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(coverage); _mockDataColectionEvents.Raise(x => x.SessionStart += null, new SessionStartEventArgs(sessionStartProperties)); - _mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is(y => y.TestModule.Contains("abc.dll")), It.IsAny()), Times.Once); + _mockCoverageWrapper.Verify(x => x.CreateCoverage(It.Is(y => y.TestModule.Contains("abc.dll")), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); _mockCoverageWrapper.Verify(x => x.PrepareModules(It.IsAny()), Times.Once); } [Fact] public void OnSessionEndShouldSendGetCoverageReportToTestPlatform() { - _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), new CoverageWrapper(), _mockCountDownEventFactory.Object); + 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())); + return serviceCollection; + }; + _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), new CoverageWrapper(), _mockCountDownEventFactory.Object, serviceCollectionFactory); _coverletCoverageDataCollector.Initialize( _configurationElement, _mockDataColectionEvents.Object, @@ -121,7 +169,21 @@ public void OnSessionEndShouldSendGetCoverageReportToTestPlatform() [InlineData("json,cobertura,lcov", 3)] public void OnSessionEndShouldSendCoverageReportsForMultipleFormatsToTestPlatform(string formats, int sendReportsCount) { - _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), new CoverageWrapper(), _mockCountDownEventFactory.Object); + Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => + { + IServiceCollection serviceCollection = new ServiceCollection(); + Mock 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(_ => new CoverletLogger(eqtTrace, logger)); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(serviceProvider => new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService())); + 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(); Mock mockDataCollectionSink = new Mock(); @@ -159,7 +221,21 @@ public void OnSessionEndShouldSendCoverageReportsForMultipleFormatsToTestPlatfor [Fact] public void OnSessionStartShouldLogWarningIfInstrumentationFailed() { - _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object); + Func serviceCollectionFactory = (TestPlatformEqtTrace eqtTrace, TestPlatformLogger logger, string testModule) => + { + IServiceCollection serviceCollection = new ServiceCollection(); + Mock 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())); + return serviceCollection; + }; + _coverletCoverageDataCollector = new CoverletCoverageCollector(new TestPlatformEqtTrace(), _mockCoverageWrapper.Object, _mockCountDownEventFactory.Object, serviceCollectionFactory); _coverletCoverageDataCollector.Initialize( _configurationElement, _mockDataColectionEvents.Object, diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.ExcludeFromCoverageAttribute.cs b/test/coverlet.core.tests/Coverage/CoverageTests.ExcludeFromCoverageAttribute.cs index 19e1d0ca4..c4c48876f 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.ExcludeFromCoverageAttribute.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.ExcludeFromCoverageAttribute.cs @@ -27,8 +27,15 @@ public void TestCoverageSkipModule__AssemblyMarkedAsExcludeFromCodeCoverage() var loggerMock = new Mock(); string excludedbyattributeDll = Directory.GetFiles(Path.Combine(Directory.GetCurrentDirectory(), "TestAssets"), "coverlet.tests.projectsample.excludedbyattribute.dll").First(); + + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, + new SourceRootTranslator(excludedbyattributeDll, new Mock().Object, new FileSystem())); + // test skip module include test assembly feature - var coverage = new Coverage(excludedbyattributeDll, new string[] { "[coverlet.tests.projectsample.excludedbyattribute*]*" }, Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), true, false, string.Empty, false, loggerMock.Object, _instrumentationHelper, partialMockFileSystem.Object); + var coverage = new Coverage(excludedbyattributeDll, new string[] { "[coverlet.tests.projectsample.excludedbyattribute*]*" }, Array.Empty(), Array.Empty(), + Array.Empty(), Array.Empty(), true, false, string.Empty, false, loggerMock.Object, instrumentationHelper, partialMockFileSystem.Object, + new SourceRootTranslator(loggerMock.Object, new FileSystem())); CoveragePrepareResult result = coverage.PrepareModules(); Assert.Empty(result.Results); loggerMock.Verify(l => l.LogVerbose(It.IsAny())); diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.cs b/test/coverlet.core.tests/Coverage/CoverageTests.cs index 63b765a8b..05ab418bd 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.cs @@ -10,7 +10,6 @@ namespace Coverlet.Core.Tests { public partial class CoverageTests { - private readonly InstrumentationHelper _instrumentationHelper = new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object); private readonly Mock _mockLogger = new Mock(); [Fact] @@ -25,8 +24,12 @@ public void TestCoverage() File.Copy(pdb, Path.Combine(directory.FullName, Path.GetFileName(pdb)), true); // TODO: Find a way to mimick hits + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, + new SourceRootTranslator(module, new Mock().Object, new FileSystem())); - var coverage = new Coverage(Path.Combine(directory.FullName, Path.GetFileName(module)), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), false, false, string.Empty, false, _mockLogger.Object, _instrumentationHelper, new FileSystem()); + var coverage = new Coverage(Path.Combine(directory.FullName, Path.GetFileName(module)), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), + Array.Empty(), false, false, string.Empty, false, _mockLogger.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(_mockLogger.Object, new FileSystem())); coverage.PrepareModules(); var result = coverage.GetCoverageResult(); @@ -47,7 +50,13 @@ public void TestCoverageWithTestAssembly() File.Copy(module, Path.Combine(directory.FullName, Path.GetFileName(module)), true); File.Copy(pdb, Path.Combine(directory.FullName, Path.GetFileName(pdb)), true); - var coverage = new Coverage(Path.Combine(directory.FullName, Path.GetFileName(module)), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), true, false, string.Empty, false, _mockLogger.Object, _instrumentationHelper, new FileSystem()); + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, + new SourceRootTranslator(module, new Mock().Object, new FileSystem())); + + var coverage = new Coverage(Path.Combine(directory.FullName, Path.GetFileName(module)), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), + Array.Empty(), true, false, string.Empty, false, _mockLogger.Object, instrumentationHelper, new FileSystem(), + new SourceRootTranslator(module, _mockLogger.Object, new FileSystem())); coverage.PrepareModules(); var result = coverage.GetCoverageResult(); diff --git a/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs b/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs index 579e6caf3..d29b4bf17 100644 --- a/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs +++ b/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs @@ -74,8 +74,6 @@ async public static Task Run(Func callM throw new ArgumentNullException(nameof(persistPrepareResultToFile)); } - SetTestContainer(disableRestoreModules); - // Rename test file to avoid locks string location = typeof(T).Assembly.Location; string fileName = Path.ChangeExtension($"testgen_{Path.GetFileNameWithoutExtension(Path.GetRandomFileName())}", ".dll"); @@ -85,6 +83,8 @@ async public static Task Run(Func callM File.Copy(location, newPath); File.Copy(Path.ChangeExtension(location, ".pdb"), Path.ChangeExtension(newPath, ".pdb")); + SetTestContainer(newPath, disableRestoreModules); + static string[] defaultFilters(string _) => Array.Empty(); // Instrument module Coverage coverage = new Coverage(newPath, @@ -98,7 +98,8 @@ async public static Task Run(Func callM { "[xunit.*]*", "[coverlet.*]*" - }).ToArray(), Array.Empty(), Array.Empty(), true, false, "", false, new Logger(logFile), _processWideContainer.GetService(), _processWideContainer.GetService()); + }).ToArray(), Array.Empty(), Array.Empty(), true, false, "", false, new Logger(logFile), + _processWideContainer.GetService(), _processWideContainer.GetService(), _processWideContainer.GetService()); CoveragePrepareResult prepareResult = coverage.PrepareModules(); Assert.Single(prepareResult.Results); @@ -128,7 +129,7 @@ async public static Task Run(Func callM return prepareResult; } - private static void SetTestContainer(bool disableRestoreModules = false) + private static void SetTestContainer(string testModule = null, bool disableRestoreModules = false) { LazyInitializer.EnsureInitialized(ref _processWideContainer, () => { @@ -147,6 +148,10 @@ private static void SetTestContainer(bool disableRestoreModules = false) { serviceCollection.AddSingleton(); } + serviceCollection.AddSingleton(serviceProvider => + string.IsNullOrEmpty(testModule) ? + new SourceRootTranslator(serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()) : + new SourceRootTranslator(testModule, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService())); return serviceCollection.BuildServiceProvider(); }); @@ -244,8 +249,8 @@ public void LogWarning(string message) class InstrumentationHelperForDebugging : InstrumentationHelper { - public InstrumentationHelperForDebugging(IProcessExitHandler processExitHandler, IRetryHelper retryHelper, IFileSystem fileSystem, ILogger logger) - : base(processExitHandler, retryHelper, fileSystem, logger) + public InstrumentationHelperForDebugging(IProcessExitHandler processExitHandler, IRetryHelper retryHelper, IFileSystem fileSystem, ILogger logger, ISourceRootTranslator sourceTranslator) + : base(processExitHandler, retryHelper, fileSystem, logger, sourceTranslator) { } diff --git a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs index 54bbea1b8..0de6e03a1 100644 --- a/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/InstrumentationHelperTests.cs @@ -10,7 +10,8 @@ namespace Coverlet.Core.Helpers.Tests { public class InstrumentationHelperTests { - private readonly InstrumentationHelper _instrumentationHelper = new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object); + private readonly InstrumentationHelper _instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, new SourceRootTranslator(typeof(InstrumentationHelperTests).Assembly.Location, new Mock().Object, new FileSystem())); [Fact] public void TestGetDependencies() diff --git a/test/coverlet.core.tests/Helpers/RetryHelperTests.cs b/test/coverlet.core.tests/Helpers/RetryHelperTests.cs index 2b547623f..93ce3359c 100644 --- a/test/coverlet.core.tests/Helpers/RetryHelperTests.cs +++ b/test/coverlet.core.tests/Helpers/RetryHelperTests.cs @@ -1,8 +1,6 @@ using System; -using System.IO; using Xunit; -using Coverlet.Core.Helpers; namespace Coverlet.Core.Helpers.Tests { diff --git a/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs b/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs new file mode 100644 index 000000000..08e103701 --- /dev/null +++ b/test/coverlet.core.tests/Helpers/SourceRootTranslatorTests.cs @@ -0,0 +1,64 @@ +using System.IO; + +using Coverlet.Core.Abstracts; +using Coverlet.Tests.Xunit.Extensions; +using Moq; +using Xunit; + +namespace Coverlet.Core.Helpers.Tests +{ + public class SourceRootTranslatorTests + { + [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"; + Mock logger = new Mock(); + Mock 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") return true; + return false; + }); + fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(File.ReadAllLines(@"TestAssets/CoverletSourceRootsMappingTest")); + SourceRootTranslator translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.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)); + } + + [Fact] + public void Translate_EmptyFile() + { + string fileToTranslate = "/_/src/coverlet.core/obj/Debug/netstandard2.0/coverlet.core.pdb"; + Mock logger = new Mock(); + Mock fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => + { + if (p == "testLib.dll" || p == "CoverletSourceRootsMapping") return true; + return false; + }); + fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(new string[0]); + SourceRootTranslator translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object); + Assert.Equal(fileToTranslate, translator.ResolveFilePath(fileToTranslate)); + } + + [Fact] + public void Translate_MalformedFile() + { + string fileToTranslate = "/_/src/coverlet.core/obj/Debug/netstandard2.0/coverlet.core.pdb"; + Mock logger = new Mock(); + Mock fileSystem = new Mock(); + fileSystem.Setup(f => f.Exists(It.IsAny())).Returns((string p) => + { + if (p == "testLib.dll" || p == "CoverletSourceRootsMapping") return true; + return false; + }); + fileSystem.Setup(f => f.ReadAllLines(It.IsAny())).Returns(new string[1] { "malformedRow" }); + SourceRootTranslator translator = new SourceRootTranslator("testLib.dll", logger.Object, fileSystem.Object); + Assert.Equal(fileToTranslate, translator.ResolveFilePath(fileToTranslate)); + logger.Verify(l => l.LogWarning(It.IsAny()), Times.Once); + } + } +} diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs index 217fa0651..5e51569b1 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs @@ -21,7 +21,6 @@ namespace Coverlet.Core.Instrumentation.Tests { public class InstrumenterTests { - private readonly InstrumentationHelper _instrumentationHelper = new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object); private readonly Mock _mockLogger = new Mock(); [ConditionalFact] @@ -72,9 +71,11 @@ public void TestCoreLibInstrumentation() } } }); - - InstrumentationHelper instrumentationHelper = new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), partialMockFileSystem.Object, _mockLogger.Object); - Instrumenter instrumenter = new Instrumenter(Path.Combine(directory.FullName, files[0]), "_coverlet_instrumented", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), false, _mockLogger.Object, instrumentationHelper, partialMockFileSystem.Object); + var sourceRootTranslator = new SourceRootTranslator(_mockLogger.Object, new FileSystem()); + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), partialMockFileSystem.Object, _mockLogger.Object, sourceRootTranslator); + Instrumenter instrumenter = new Instrumenter(Path.Combine(directory.FullName, files[0]), "_coverlet_instrumented", Array.Empty(), Array.Empty(), Array.Empty(), + Array.Empty(), false, _mockLogger.Object, instrumentationHelper, partialMockFileSystem.Object, sourceRootTranslator); Assert.True(instrumenter.CanInstrument()); InstrumenterResult result = instrumenter.Instrument(); @@ -216,8 +217,12 @@ private InstrumenterTest CreateInstrumentor(bool fakeCoreLibModule = false, stri File.Copy(module, Path.Combine(directory.FullName, destModule), true); File.Copy(pdb, Path.Combine(directory.FullName, destPdb), true); + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, new SourceRootTranslator(new Mock().Object, new FileSystem())); + module = Path.Combine(directory.FullName, destModule); - Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty(), Array.Empty(), Array.Empty(), attributesToIgnore, false, _mockLogger.Object, _instrumentationHelper, new FileSystem()); + Instrumenter instrumenter = new Instrumenter(module, identifier, Array.Empty(), Array.Empty(), Array.Empty(), attributesToIgnore, false, + _mockLogger.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(_mockLogger.Object, new FileSystem())); return new InstrumenterTest { Instrumenter = instrumenter, @@ -388,16 +393,28 @@ public void SkipEmbeddedPpdbWithoutLocalSource() { string xunitDll = Directory.GetFiles(Directory.GetCurrentDirectory(), "xunit.core.dll").First(); var loggerMock = new Mock(); - Instrumenter instrumenter = new Instrumenter(xunitDll, "_xunit_instrumented", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), false, loggerMock.Object, _instrumentationHelper, new FileSystem()); - Assert.True(_instrumentationHelper.HasPdb(xunitDll, out bool embedded)); + + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, + new SourceRootTranslator(xunitDll, new Mock().Object, new FileSystem())); + + Instrumenter instrumenter = new Instrumenter(xunitDll, "_xunit_instrumented", Array.Empty(), Array.Empty(), Array.Empty(), + Array.Empty(), false, loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(xunitDll, loggerMock.Object, new FileSystem())); + Assert.True(instrumentationHelper.HasPdb(xunitDll, out bool embedded)); Assert.True(embedded); Assert.False(instrumenter.CanInstrument()); loggerMock.Verify(l => l.LogVerbose(It.IsAny())); // Default case string sample = Directory.GetFiles(Directory.GetCurrentDirectory(), "coverlet.tests.projectsample.empty.dll").First(); - instrumenter = new Instrumenter(sample, "_coverlet_tests_projectsample_empty", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), false, loggerMock.Object, _instrumentationHelper, new FileSystem()); - Assert.True(_instrumentationHelper.HasPdb(sample, out embedded)); + instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, + new SourceRootTranslator(sample, new Mock().Object, new FileSystem())); + + instrumenter = new Instrumenter(sample, "_coverlet_tests_projectsample_empty", Array.Empty(), Array.Empty(), Array.Empty(), + Array.Empty(), false, loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(sample, loggerMock.Object, new FileSystem())); + + Assert.True(instrumentationHelper.HasPdb(sample, out embedded)); Assert.False(embedded); Assert.True(instrumenter.CanInstrument()); loggerMock.VerifyNoOtherCalls(); @@ -436,10 +453,12 @@ public void SkipPpdbWithoutLocalSource() } }); - InstrumentationHelper instrumentationHelper = new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), partialMockFileSystem.Object, _mockLogger.Object); + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), partialMockFileSystem.Object, _mockLogger.Object, new SourceRootTranslator(_mockLogger.Object, new FileSystem())); string sample = Directory.GetFiles(Path.Combine(Directory.GetCurrentDirectory(), "TestAssets"), dllFileName).First(); var loggerMock = new Mock(); - Instrumenter instrumenter = new Instrumenter(sample, "_75d9f96508d74def860a568f426ea4a4_instrumented", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), false, loggerMock.Object, instrumentationHelper, partialMockFileSystem.Object); + Instrumenter instrumenter = new Instrumenter(sample, "_75d9f96508d74def860a568f426ea4a4_instrumented", Array.Empty(), Array.Empty(), Array.Empty(), + Array.Empty(), false, loggerMock.Object, instrumentationHelper, partialMockFileSystem.Object, new SourceRootTranslator(loggerMock.Object, new FileSystem())); Assert.True(instrumentationHelper.HasPdb(sample, out bool embedded)); Assert.False(embedded); Assert.False(instrumenter.CanInstrument()); @@ -450,7 +469,13 @@ public void SkipPpdbWithoutLocalSource() public void TestInstrument_MissingModule() { var loggerMock = new Mock(); - var instrumenter = new Instrumenter("test", "_test_instrumented", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), false, loggerMock.Object, _instrumentationHelper, new FileSystem()); + + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, + new SourceRootTranslator(new Mock().Object, new FileSystem())); + + var instrumenter = new Instrumenter("test", "_test_instrumented", Array.Empty(), Array.Empty(), Array.Empty(), + Array.Empty(), false, loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(loggerMock.Object, new FileSystem())); Assert.False(instrumenter.CanInstrument()); loggerMock.Verify(l => l.LogWarning(It.IsAny())); } @@ -467,7 +492,13 @@ public void TestInstrument_AssemblyMarkedAsExcludeFromCodeCoverage() var loggerMock = new Mock(); string excludedbyattributeDll = Directory.GetFiles(Path.Combine(Directory.GetCurrentDirectory(), "TestAssets"), "coverlet.tests.projectsample.excludedbyattribute.dll").First(); - Instrumenter instrumenter = new Instrumenter(excludedbyattributeDll, "_xunit_excludedbyattribute", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), false, loggerMock.Object, _instrumentationHelper, partialMockFileSystem.Object); + + InstrumentationHelper instrumentationHelper = + new InstrumentationHelper(new ProcessExitHandler(), new RetryHelper(), new FileSystem(), new Mock().Object, + new SourceRootTranslator(new Mock().Object, new FileSystem())); + + Instrumenter instrumenter = new Instrumenter(excludedbyattributeDll, "_xunit_excludedbyattribute", Array.Empty(), Array.Empty(), Array.Empty(), + Array.Empty(), false, loggerMock.Object, instrumentationHelper, partialMockFileSystem.Object, new SourceRootTranslator(loggerMock.Object, new FileSystem())); InstrumenterResult result = instrumenter.Instrument(); Assert.Empty(result.Documents); loggerMock.Verify(l => l.LogVerbose(It.IsAny())); diff --git a/test/coverlet.core.tests/TestAssets/CoverletSourceRootsMappingTest b/test/coverlet.core.tests/TestAssets/CoverletSourceRootsMappingTest new file mode 100644 index 000000000..15a93306a Binary files /dev/null and b/test/coverlet.core.tests/TestAssets/CoverletSourceRootsMappingTest differ diff --git a/test/coverlet.core.tests/coverlet.core.tests.csproj b/test/coverlet.core.tests/coverlet.core.tests.csproj index f9c0ce3ef..64fa8a2ce 100644 --- a/test/coverlet.core.tests/coverlet.core.tests.csproj +++ b/test/coverlet.core.tests/coverlet.core.tests.csproj @@ -33,10 +33,6 @@ - - - - PreserveNewest @@ -44,6 +40,9 @@ PreserveNewest + + Always + PreserveNewest diff --git a/test/coverlet.tests.xunit.extensions/SkipOnOS.cs b/test/coverlet.tests.xunit.extensions/SkipOnOS.cs index 82b030fb8..329a799e4 100644 --- a/test/coverlet.tests.xunit.extensions/SkipOnOS.cs +++ b/test/coverlet.tests.xunit.extensions/SkipOnOS.cs @@ -3,20 +3,20 @@ namespace Coverlet.Tests.Xunit.Extensions { - [Flags] public enum OS { - Linux = 1, - MacOS = 2, - Windows = 4 + 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; - public SkipOnOSAttribute(OS os) => _os = os; + public SkipOnOSAttribute(OS os, string reason = "") => (_os, _reason) = (os, reason); public bool IsMet => _os switch { @@ -26,6 +26,6 @@ public class SkipOnOSAttribute : Attribute, ITestCondition _ => throw new NotSupportedException($"Not supported OS {_os}") }; - public string SkipReason => "OS not supported"; + public string SkipReason => $"OS not supported{(string.IsNullOrEmpty(_reason) ? "" : $", {_reason}")}"; } }