Files
coverlet/src/coverlet.core/Coverage.cs
T
Marco Rossignoli 4f3f25fda9 Move logger under abstracts (#601)
Move logger under abstracts
2019-10-30 14:48:30 +01:00

455 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Coverlet.Core.Abstracts;
using Coverlet.Core.Instrumentation;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Coverlet.Core
{
public class Coverage
{
private string _module;
private string _identifier;
private string[] _includeFilters;
private string[] _includeDirectories;
private string[] _excludeFilters;
private string[] _excludedSourceFiles;
private string[] _excludeAttributes;
private bool _includeTestAssembly;
private bool _singleHit;
private string _mergeWith;
private bool _useSourceLink;
private ILogger _logger;
private IInstrumentationHelper _instrumentationHelper;
private IFileSystem _fileSystem;
private List<InstrumenterResult> _results;
public string Identifier
{
get { return _identifier; }
}
public Coverage(string module,
string[] includeFilters,
string[] includeDirectories,
string[] excludeFilters,
string[] excludedSourceFiles,
string[] excludeAttributes,
bool includeTestAssembly,
bool singleHit,
string mergeWith,
bool useSourceLink,
ILogger logger,
IInstrumentationHelper instrumentationHelper,
IFileSystem fileSystem)
{
_module = module;
_includeFilters = includeFilters;
_includeDirectories = includeDirectories ?? Array.Empty<string>();
_excludeFilters = excludeFilters;
_excludedSourceFiles = excludedSourceFiles;
_excludeAttributes = excludeAttributes;
_includeTestAssembly = includeTestAssembly;
_singleHit = singleHit;
_mergeWith = mergeWith;
_useSourceLink = useSourceLink;
_logger = logger;
_instrumentationHelper = instrumentationHelper;
_fileSystem = fileSystem;
_identifier = Guid.NewGuid().ToString();
_results = new List<InstrumenterResult>();
}
public Coverage(CoveragePrepareResult prepareResult, ILogger logger, IInstrumentationHelper instrumentationHelper, IFileSystem fileSystem)
{
_identifier = prepareResult.Identifier;
_module = prepareResult.Module;
_mergeWith = prepareResult.MergeWith;
_useSourceLink = prepareResult.UseSourceLink;
_results = new List<InstrumenterResult>(prepareResult.Results);
_logger = logger;
_instrumentationHelper = instrumentationHelper;
_fileSystem = fileSystem;
}
public CoveragePrepareResult PrepareModules()
{
string[] modules = _instrumentationHelper.GetCoverableModules(_module, _includeDirectories, _includeTestAssembly);
Array.ForEach(_excludeFilters ?? Array.Empty<string>(), filter => _logger.LogVerbose($"Excluded module filter '{filter}'"));
Array.ForEach(_includeFilters ?? Array.Empty<string>(), filter => _logger.LogVerbose($"Included module filter '{filter}'"));
Array.ForEach(_excludedSourceFiles ?? Array.Empty<string>(), filter => _logger.LogVerbose($"Excluded source files filter '{filter}'"));
_excludeFilters = _excludeFilters?.Where(f => _instrumentationHelper.IsValidFilterExpression(f)).ToArray();
_includeFilters = _includeFilters?.Where(f => _instrumentationHelper.IsValidFilterExpression(f)).ToArray();
foreach (var module in modules)
{
if (_instrumentationHelper.IsModuleExcluded(module, _excludeFilters) ||
!_instrumentationHelper.IsModuleIncluded(module, _includeFilters))
{
_logger.LogVerbose($"Excluded module: '{module}'");
continue;
}
var instrumenter = new Instrumenter(module, _identifier, _excludeFilters, _includeFilters, _excludedSourceFiles, _excludeAttributes, _singleHit, _logger, _instrumentationHelper, _fileSystem);
if (instrumenter.CanInstrument())
{
_instrumentationHelper.BackupOriginalModule(module, _identifier);
// Guard code path and restore if instrumentation fails.
try
{
InstrumenterResult result = instrumenter.Instrument();
if (!instrumenter.SkipModule)
{
_results.Add(result);
_logger.LogVerbose($"Instrumented module: '{module}'");
}
}
catch (Exception ex)
{
_logger.LogWarning($"Unable to instrument module: {module} because : {ex.Message}");
_instrumentationHelper.RestoreOriginalModule(module, _identifier);
}
}
}
return new CoveragePrepareResult()
{
Identifier = _identifier,
Module = _module,
MergeWith = _mergeWith,
UseSourceLink = _useSourceLink,
Results = _results.ToArray()
};
}
public CoverageResult GetCoverageResult()
{
CalculateCoverage();
Modules modules = new Modules();
foreach (var result in _results)
{
Documents documents = new Documents();
foreach (var doc in result.Documents.Values)
{
// Construct Line Results
foreach (var line in doc.Lines.Values)
{
if (documents.TryGetValue(doc.Path, out Classes classes))
{
if (classes.TryGetValue(line.Class, out Methods methods))
{
if (methods.TryGetValue(line.Method, out Method method))
{
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, line.Hits);
}
else
{
documents[doc.Path][line.Class].Add(line.Method, new Method());
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, line.Hits);
}
}
else
{
documents[doc.Path].Add(line.Class, new Methods());
documents[doc.Path][line.Class].Add(line.Method, new Method());
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, line.Hits);
}
}
else
{
documents.Add(doc.Path, new Classes());
documents[doc.Path].Add(line.Class, new Methods());
documents[doc.Path][line.Class].Add(line.Method, new Method());
documents[doc.Path][line.Class][line.Method].Lines.Add(line.Number, line.Hits);
}
}
// Construct Branch Results
foreach (var branch in doc.Branches.Values)
{
if (documents.TryGetValue(doc.Path, out Classes classes))
{
if (classes.TryGetValue(branch.Class, out Methods methods))
{
if (methods.TryGetValue(branch.Method, out Method method))
{
method.Branches.Add(new BranchInfo
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
);
}
else
{
documents[doc.Path][branch.Class].Add(branch.Method, new Method());
documents[doc.Path][branch.Class][branch.Method].Branches.Add(new BranchInfo
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
);
}
}
else
{
documents[doc.Path].Add(branch.Class, new Methods());
documents[doc.Path][branch.Class].Add(branch.Method, new Method());
documents[doc.Path][branch.Class][branch.Method].Branches.Add(new BranchInfo
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
);
}
}
else
{
documents.Add(doc.Path, new Classes());
documents[doc.Path].Add(branch.Class, new Methods());
documents[doc.Path][branch.Class].Add(branch.Method, new Method());
documents[doc.Path][branch.Class][branch.Method].Branches.Add(new BranchInfo
{ Line = branch.Number, Hits = branch.Hits, Offset = branch.Offset, EndOffset = branch.EndOffset, Path = branch.Path, Ordinal = branch.Ordinal }
);
}
}
}
modules.Add(Path.GetFileName(result.ModulePath), documents);
_instrumentationHelper.RestoreOriginalModule(result.ModulePath, _identifier);
}
// In case of anonymous delegate compiler generate a custom class and passes it as type.method delegate.
// If in delegate method we've a branches we need to move these to "actual" class/method that use it.
// We search "method" with same "Line" of closure class method and add missing branches to it,
// in this way we correctly report missing branch inside compiled generated anonymous delegate.
List<string> compileGeneratedClassToRemove = null;
foreach (var module in modules)
{
foreach (var document in module.Value)
{
foreach (var @class in document.Value)
{
foreach (var method in @class.Value)
{
foreach (var branch in method.Value.Branches)
{
if (BranchInCompilerGeneratedClass(method.Key))
{
Method actualMethod = GetMethodWithSameLineInSameDocument(document.Value, @class.Key, branch.Line);
if (actualMethod is null)
{
continue;
}
actualMethod.Branches.Add(branch);
if (compileGeneratedClassToRemove is null)
{
compileGeneratedClassToRemove = new List<string>();
}
if (!compileGeneratedClassToRemove.Contains(@class.Key))
{
compileGeneratedClassToRemove.Add(@class.Key);
}
}
}
}
}
}
}
// After method/branches analysis of compiled generated class we can remove noise from reports
if (!(compileGeneratedClassToRemove is null))
{
foreach (var module in modules)
{
foreach (var document in module.Value)
{
foreach (var classToRemove in compileGeneratedClassToRemove)
{
document.Value.Remove(classToRemove);
}
}
}
}
var coverageResult = new CoverageResult { Identifier = _identifier, Modules = modules, InstrumentedResults = _results };
if (!string.IsNullOrEmpty(_mergeWith) && !string.IsNullOrWhiteSpace(_mergeWith) && _fileSystem.Exists(_mergeWith))
{
string json = _fileSystem.ReadAllText(_mergeWith);
coverageResult.Merge(JsonConvert.DeserializeObject<Modules>(json));
}
return coverageResult;
}
private bool BranchInCompilerGeneratedClass(string methodName)
{
foreach (var instrumentedResult in _results)
{
if (instrumentedResult.BranchesInCompiledGeneratedClass.Contains(methodName))
{
return true;
}
}
return false;
}
private Method GetMethodWithSameLineInSameDocument(Classes documentClasses, string compilerGeneratedClassName, int branchLine)
{
foreach (var @class in documentClasses)
{
if (@class.Key == compilerGeneratedClassName)
{
continue;
}
foreach (var method in @class.Value)
{
foreach (var line in method.Value.Lines)
{
if (line.Key == branchLine)
{
return method.Value;
}
}
}
}
return null;
}
private void CalculateCoverage()
{
foreach (var result in _results)
{
if (!_fileSystem.Exists(result.HitsFilePath))
{
// Hits file could be missed mainly for two reason
// 1) Issue during module Unload()
// 2) Instrumented module is never loaded or used so we don't have any hit to register and
// module tracker is never used
_logger.LogVerbose($"Hits file:'{result.HitsFilePath}' not found for module: '{result.Module}'");
continue;
}
List<Document> documents = result.Documents.Values.ToList();
if (_useSourceLink && result.SourceLink != null)
{
var jObject = JObject.Parse(result.SourceLink)["documents"];
var sourceLinkDocuments = JsonConvert.DeserializeObject<Dictionary<string, string>>(jObject.ToString());
foreach (var document in documents)
{
document.Path = GetSourceLinkUrl(sourceLinkDocuments, document.Path);
}
}
List<(int docIndex, int line)> zeroHitsLines = new List<(int docIndex, int line)>();
var documentsList = result.Documents.Values.ToList();
using (var fs = _fileSystem.NewFileStream(result.HitsFilePath, FileMode.Open))
using (var br = new BinaryReader(fs))
{
int hitCandidatesCount = br.ReadInt32();
// TODO: hitCandidatesCount should be verified against result.HitCandidates.Count
for (int i = 0; i < hitCandidatesCount; ++i)
{
var hitLocation = result.HitCandidates[i];
var document = documentsList[hitLocation.docIndex];
int hits = br.ReadInt32();
if (hitLocation.isBranch)
{
var branch = document.Branches[new BranchKey(hitLocation.start, hitLocation.end)];
branch.Hits += hits;
}
else
{
for (int j = hitLocation.start; j <= hitLocation.end; j++)
{
var line = document.Lines[j];
line.Hits += hits;
// We register 0 hit lines for later cleanup false positive of nested lambda closures
if (hits == 0)
{
zeroHitsLines.Add((hitLocation.docIndex, line.Number));
}
}
}
}
}
// Cleanup nested state machine false positive hits
foreach (var (docIndex, line) in zeroHitsLines)
{
foreach (var lineToCheck in documentsList[docIndex].Lines)
{
if (lineToCheck.Key == line)
{
lineToCheck.Value.Hits = 0;
}
}
}
_instrumentationHelper.DeleteHitsFile(result.HitsFilePath);
_logger.LogVerbose($"Hit file '{result.HitsFilePath}' deleted");
}
}
private string GetSourceLinkUrl(Dictionary<string, string> sourceLinkDocuments, string document)
{
if (sourceLinkDocuments.TryGetValue(document, out string url))
{
return url;
}
var keyWithBestMatch = string.Empty;
var relativePathOfBestMatch = string.Empty;
foreach (var sourceLinkDocument in sourceLinkDocuments)
{
string key = sourceLinkDocument.Key;
if (Path.GetFileName(key) != "*") continue;
string directoryDocument = Path.GetDirectoryName(document);
string sourceLinkRoot = Path.GetDirectoryName(key);
string relativePath = "";
// if document is on repo root we skip relative path calculation
if (directoryDocument != sourceLinkRoot)
{
if (!directoryDocument.StartsWith(sourceLinkRoot + Path.DirectorySeparatorChar))
continue;
relativePath = directoryDocument.Substring(sourceLinkRoot.Length + 1);
}
if (relativePathOfBestMatch.Length == 0)
{
keyWithBestMatch = sourceLinkDocument.Key;
relativePathOfBestMatch = relativePath;
}
if (relativePath.Length < relativePathOfBestMatch.Length)
{
keyWithBestMatch = sourceLinkDocument.Key;
relativePathOfBestMatch = relativePath;
}
}
relativePathOfBestMatch = relativePathOfBestMatch == "." ? string.Empty : relativePathOfBestMatch;
string replacement = Path.Combine(relativePathOfBestMatch, Path.GetFileName(document));
replacement = replacement.Replace('\\', '/');
url = sourceLinkDocuments[keyWithBestMatch];
return url.Replace("*", replacement);
}
}
}