using System; using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; using System.Reflection; using System.Text; using System.Linq; using UnityEngine; using UnityEngine.TestTools; using UnityEditor.TestTools.TestRunner.Api; using OpenCover.Framework.Model; using Module = OpenCover.Framework.Model.Module; using ModelFile = OpenCover.Framework.Model.File; using File = System.IO.File; using UnityEditor.TestTools.CodeCoverage.Utils; using NUnit.Framework; namespace UnityEditor.TestTools.CodeCoverage.OpenCover { internal class OpenCoverReporter : ICoverageReporter { private readonly ICoverageStatsProvider m_StatsProvider; private readonly ICoverageReporterFilter m_ReporterFilter; private OpenCoverResultWriter m_Writer; private List m_ExcludedMethods = null; private List m_ExcludedTypes = null; private ITestAdaptor m_CurrentTestData; private static readonly Dictionary m_Operators = new Dictionary { { "op_Addition", "operator+" }, { "op_UnaryPlus", "operator+" }, { "op_Increment", "operator++" }, { "op_Subtraction", "operator-" }, { "op_UnaryNegation", "operator-" }, { "op_Decrement", "operator--" }, { "op_Multiply", "operator*" }, { "op_Division", "operator/" }, { "op_Modulus", "operator%" }, { "op_ExclusiveOr", "operator^" }, { "op_BitwiseAnd", "operator&" }, { "op_BitwiseOr", "operator|" }, { "op_LeftShift", "operator<<" }, { "op_RightShift", "operator>>" }, { "op_Equality", "operator==" }, { "op_Inequality", "operator!=" }, { "op_GreaterThan", "operator>" }, { "op_LessThan", "operator<" }, { "op_GreaterThanOrEqual", "operator>=" }, { "op_LessThanOrEqual", "operator<=" }, { "op_OnesComplement", "operator~" }, { "op_LogicalNot", "operator!" }, { "op_True", "operator true" }, { "op_False", "operator false" } }; public OpenCoverReporter() : this(new OpenCoverReporterFilter(), new CoverageStats()) { } internal OpenCoverReporter(ICoverageReporterFilter reporterFilter, ICoverageStatsProvider statsProvider) { m_ReporterFilter = reporterFilter; m_StatsProvider = statsProvider; } public ICoverageReporterFilter GetReporterFilter() { return m_ReporterFilter; } public void OnBeforeAssemblyReload() { if (m_CurrentTestData != null && m_ReporterFilter.ShouldGenerateTestReferences()) { OutputVisitedCoverageReport(CoverageRunData.instance.isRecording, true, m_CurrentTestData); } else { OutputVisitedCoverageReport(true, !CoverageRunData.instance.isRecording); } } public void OnCoverageRecordingPaused() { OutputVisitedCoverageReport(); } public void OnInitialise(CoverageSettings settings) { if (!m_ReporterFilter.ShouldGenerateTestReferences() && settings.resetCoverageData) { m_StatsProvider.ResetAll(); } m_ReporterFilter.SetupFiltering(); CoverageRunData.instance.testSuite = TestContext.Parameters.Get("platform"); if (m_Writer == null) { m_Writer = new OpenCoverResultWriter(settings); } m_Writer.SetupCoveragePaths(); } public void OnRunStarted(ITestAdaptor testsToRun) { Events.InvokeOnCoverageSessionStarted(); CoverageRunData.instance.testSuite = TestContext.Parameters.Get("platform"); if (m_Writer != null) { if (!CommandLineManager.instance.dontClear) m_Writer.ClearCoverageFolderIfExists(); m_Writer.SetupCoveragePaths(); OutputFullEmptyReport(); } } public void OnRunFinished(ITestResultAdaptor testResults) { if (CoverageRunData.instance.isRecording || !m_ReporterFilter.ShouldGenerateTestReferences()) { OutputVisitedCoverageReport(false); } Events.InvokeOnCoverageSessionFinished(); } public void OnTestStarted(ITestAdaptor test) { if (m_ReporterFilter.ShouldGenerateTestReferences()) { m_CurrentTestData = test; m_StatsProvider.ResetAll(); } } public void OnTestFinished(ITestResultAdaptor result) { if (m_ReporterFilter.ShouldGenerateTestReferences()) { OutputVisitedCoverageReport(CoverageRunData.instance.isRecording, true, result.Test); } } private void OutputFullEmptyReport() { if (m_Writer != null) { // Exit if generateRootEmptyReport arg is passed and a root fullEmpty report has already been generated if (CommandLineManager.instance.generateRootEmptyReport) { string rootFullEmptyPath = m_Writer.GetRootFullEmptyPath(); if (File.Exists(rootFullEmptyPath)) return; } if (!CommandLineManager.instance.batchmode) EditorUtility.DisplayProgressBar(OpenCoverReporterStyles.ProgressTitle.text, OpenCoverReporterStyles.ProgressWritingFile.text, 0.85f); CoverageSession coverageSession = GenerateOpenCoverSession(CoverageReportType.FullEmpty); if (coverageSession != null) { m_Writer.CoverageSession = coverageSession; m_Writer.WriteCoverageSession(CoverageReportType.FullEmpty); } else { ResultsLogger.Log(ResultID.Warning_NoCoverageResultsSaved, CoverageUtils.GetFilteringLogParams(m_ReporterFilter.GetAssemblyFiltering(), m_ReporterFilter.GetPathFiltering()) ); } if (!CommandLineManager.instance.batchmode) EditorUtility.ClearProgressBar(); } } public void OutputVisitedCoverageReport(bool clearProgressBar = true, bool logNoCoverageResultsSavedWarning = true, ITestAdaptor testData = null) { if (!CommandLineManager.instance.batchmode) EditorUtility.DisplayProgressBar(OpenCoverReporterStyles.ProgressTitle.text, OpenCoverReporterStyles.ProgressWritingFile.text, 0.85f); MethodInfo testMethodInfo = testData != null ? testData.Method.MethodInfo : null; CoverageSession coverageSession = GenerateOpenCoverSession(CoverageReportType.CoveredMethodsOnly, testMethodInfo); if (coverageSession != null && m_Writer != null) { m_Writer.CoverageSession = coverageSession; m_Writer.WriteCoverageSession(CoverageReportType.CoveredMethodsOnly); } else { if (logNoCoverageResultsSavedWarning) { if (CoverageRunData.instance.isRecordingPaused) { ResultsLogger.Log(ResultID.Warning_NoVisitedCoverageResultsSavedRecordingPaused, CoverageUtils.GetFilteringLogParams(m_ReporterFilter.GetAssemblyFiltering(), m_ReporterFilter.GetPathFiltering())); } else { ResultsLogger.Log(ResultID.Warning_NoVisitedCoverageResultsSaved, CoverageUtils.GetFilteringLogParams(m_ReporterFilter.GetAssemblyFiltering(), m_ReporterFilter.GetPathFiltering())); } } } if (clearProgressBar) EditorUtility.ClearProgressBar(); } private bool IsSpecialMethod(MethodBase methodBase) { return methodBase.IsSpecialName && ((methodBase.Attributes & MethodAttributes.HideBySig) != 0); } private bool IsConstructor(MethodBase methodBase) { return IsSpecialMethod(methodBase) && methodBase.MemberType == MemberTypes.Constructor && methodBase.Name == ".ctor"; } private bool IsStaticConstructor(MethodBase methodBase) { return IsSpecialMethod(methodBase) && methodBase.IsStatic && methodBase.MemberType == MemberTypes.Constructor && methodBase.Name == ".cctor"; } private bool IsPropertySetter(MethodBase methodBase) { return IsSpecialMethod(methodBase) && methodBase.Name.StartsWith("set_"); } private bool IsPropertyGetter(MethodBase methodBase) { return IsSpecialMethod(methodBase) && methodBase.Name.StartsWith("get_"); } private bool IsOperator(MethodBase methodBase) { return IsSpecialMethod(methodBase) && methodBase.Name.StartsWith("op_"); } private bool IsAnonymousOrInnerMethod(MethodBase methodBase) { char[] invalidChars = { '<', '>' }; return methodBase.Name.IndexOfAny(invalidChars) != -1; } private string GenerateOperatorName(MethodBase methodBase) { string operatorName = string.Empty; if (!m_Operators.TryGetValue(methodBase.Name, out operatorName)) { switch (methodBase.Name) { case "op_Implicit": operatorName = $"implicit operator {GetReturnTypeName(methodBase)}"; break; case "op_Explicit": operatorName = $"explicit operator {GetReturnTypeName(methodBase)}"; break; default: operatorName = $"unknown operator {methodBase.Name}"; break; } } return operatorName; } private string GetReturnTypeName(MethodBase methodBase) { string returnTypeName = string.Empty; MethodInfo methodInfo = methodBase as MethodInfo; if (methodInfo != null) { returnTypeName = GenerateTypeName(methodInfo.ReturnType); } return returnTypeName; } internal string GenerateMethodName(MethodBase methodBase) { StringBuilder sb = new StringBuilder(); if (methodBase.IsStatic) { sb.Append("static "); } string returnTypeName = GetReturnTypeName(methodBase); if (returnTypeName != string.Empty) { sb.Append(returnTypeName); sb.Append(' '); } StringBuilder methodStringBuilder = new StringBuilder(); methodStringBuilder.Append(GenerateTypeName(methodBase.DeclaringType)); methodStringBuilder.Append("."); bool lastDotSubstituted = false; if (IsConstructor(methodBase) || IsStaticConstructor(methodBase)) { methodStringBuilder.Append(GenerateConstructorName(methodBase.DeclaringType)); } else if (IsOperator(methodBase)) { lastDotSubstituted = SubstituteLastDotWithDoubleColon(ref methodStringBuilder); methodStringBuilder.Append(GenerateOperatorName(methodBase)); } else if (IsAnonymousOrInnerMethod(methodBase)) { lastDotSubstituted = SubstituteLastDotWithDoubleColon(ref methodStringBuilder); methodStringBuilder.Append(methodBase.Name); } else { methodStringBuilder.Append(methodBase.Name); } if (!lastDotSubstituted) { SubstituteLastDotWithDoubleColon(ref methodStringBuilder); } sb.Append(methodStringBuilder.ToString()); if (methodBase.IsGenericMethodDefinition) { Type[] types = methodBase.GetGenericArguments(); sb.Append(GenerateGenericTypeList(types)); } sb.Append('('); ParameterInfo[] parameterInfos = methodBase.GetParameters(); for (int i=0; i 0) { int lastDotPos = -1; for (int i = sb.Length - 1; i >= 0; i--) { if (sb[i] == '.') { lastDotPos = i; break; } } if (lastDotPos != -1 ) { sb.Remove(lastDotPos, 1); sb.Insert(lastDotPos, "::"); substituted = true; } } return substituted; } private string GenerateTypeName(Type type) { StringBuilder sb = new StringBuilder(); if (type != null) { if (type.IsGenericTypeDefinition || type.IsGenericType) { sb.Append(GenerateGenericTypeName(type)); } else if (type.IsGenericParameter) { sb.Append(type.Name); } else { sb.Append(type.FullName); } // Replace + with / so as nested classes appear in the same file sb.Replace('+', '/'); } return sb.ToString(); } private string GenerateGenericTypeName(Type type) { StringBuilder sb = new StringBuilder(); if (type != null) { if (type.IsGenericTypeDefinition || type.IsGenericType) { // When IsGenericType the FullName includes unnecessary information and thus cannot be used. // Therefore we use the Name instead and add the Namespace at the beginning if (!type.IsGenericTypeDefinition && type.IsGenericType && type.Namespace != string.Empty) { sb.Append(type.Namespace); sb.Append('.'); } string[] splitTypes = type.IsGenericTypeDefinition ? type.FullName.Split('+') : type.Name.Split('+'); Type[] genericTypeArguments = type.GetGenericArguments(); int genericTypeArgumentIndex = 0; int numOfTypes = splitTypes.Length; for (int i = 0; i < numOfTypes; ++i) { string splitType = splitTypes[i]; int genericSeparatorIndex = splitType.LastIndexOf('`'); if (genericSeparatorIndex != -1) { sb.Append(splitType.Substring(0, genericSeparatorIndex)); string argumentIndexStr = splitType.Substring(genericSeparatorIndex+1); int numOfArguments; if (Int32.TryParse(argumentIndexStr, out numOfArguments)) { sb.Append("["); for (int j = 0; j < numOfArguments; ++j) { if (genericTypeArgumentIndex < genericTypeArguments.Length) { sb.Append($"{genericTypeArguments[genericTypeArgumentIndex++].Name}"); if (j < numOfArguments - 1) sb.Append(","); } } sb.Append("]"); } if (i < numOfTypes - 1) sb.Append("/"); } else { sb.Append(splitType); } } } } return sb.ToString(); } private string GenerateConstructorName(Type type) { StringBuilder sb = new StringBuilder(); string typeName = type.Name; int genericSeparatorIndex = typeName.LastIndexOf('`'); if (genericSeparatorIndex != -1) { sb.Append(typeName.Substring(0, genericSeparatorIndex)); } else { sb.Append(typeName); } return sb.ToString(); } private string GenerateGenericTypeList(Type[] genericTypes) { StringBuilder sb = new StringBuilder(); sb.Append('['); for (int i = 0; i < genericTypes.Length; ++i) { sb.Append(genericTypes[i].Name); if (i != genericTypes.Length - 1) sb.Append(", "); } sb.Append(']'); return sb.ToString(); } internal CoverageSession GenerateOpenCoverSession(CoverageReportType reportType, MethodInfo testMethodInfo = null) { ResultsLogger.LogSessionItem("Started OpenCover Session", LogVerbosityLevel.Info); CoverageSession coverageSession = null; UInt32 fileUID = 0; UInt32 trackedMethodUID = 0; List moduleList = new List(); Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); float progressInterval = 0.9f / assemblies.Length; float currentProgress = 0.0f; bool shouldGenerateAdditionalMetrics = m_ReporterFilter.ShouldGenerateAdditionalMetrics(); foreach (Assembly assembly in assemblies) { string assemblyName = assembly.GetName().Name.ToLower(); currentProgress += progressInterval; if (!CommandLineManager.instance.batchmode) EditorUtility.DisplayProgressBar(OpenCoverReporterStyles.ProgressTitle.text, OpenCoverReporterStyles.ProgressGatheringResults.text, currentProgress); if (!m_ReporterFilter.ShouldProcessAssembly(assemblyName)) { ResultsLogger.LogSessionItem($"Excluded assembly (Assembly Filtering): {assemblyName}", LogVerbosityLevel.Verbose); continue; } try { if (assembly.GetCustomAttribute() != null || assembly.GetCustomAttribute() != null) { ResultsLogger.LogSessionItem($"Excluded assembly (ExcludeFromCoverage): {assemblyName}", LogVerbosityLevel.Info); continue; } } catch { ResultsLogger.Log(ResultID.Warning_ExcludeAttributeAssembly, assemblyName); } List coveredClasses = new List(); List filesNotFound = new List(); Dictionary fileList = new Dictionary(); Type[] assemblyTypes = null; m_ExcludedMethods = null; m_ExcludedTypes = null; try { assemblyTypes = assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { // This exception can be thrown if some of the types from this assembly can't be loaded. If this // happens, the Types property array contains a Type for all loaded types and null for each // type that couldn't be loaded. assemblyTypes = ex.Types; m_ReporterFilter.ShouldProcessAssembly(assemblyName); } if (assemblyTypes == null) { ResultsLogger.Log(ResultID.Assert_NullAssemblyTypes); continue; } ResultsLogger.LogSessionItem($"Processing assembly: {assemblyName}", LogVerbosityLevel.Info); foreach (Type type in assemblyTypes) { // The type can be null if the ReflectionTypeLoadException has been thrown previously. if (type == null) { continue; } string className = type.FullName; try { if (type.GetCustomAttribute() != null || type.GetCustomAttribute() != null || CheckIfParentMemberIsExcluded(type)) { ResultsLogger.LogSessionItem($"Excluded class (ExcludeFromCoverage): {className}, assembly: {assemblyName}", LogVerbosityLevel.Info); if (m_ExcludedTypes == null) m_ExcludedTypes = new List(); m_ExcludedTypes.Add(type.FullName); continue; } } catch { ResultsLogger.Log(ResultID.Warning_ExcludeAttributeClass, className, assemblyName); } ResultsLogger.LogSessionItem($"Processing class: {className}, assembly: {assemblyName}", LogVerbosityLevel.Verbose); CoveredMethodStats[] classMethodStatsArray = m_StatsProvider.GetStatsFor(type); if (classMethodStatsArray.Length > 0) { List coveredMethods = new List(); foreach (CoveredMethodStats classMethodStats in classMethodStatsArray) { MethodBase method = classMethodStats.method; if (method == null) continue; string methodName = method.Name; try { if (method.GetCustomAttribute() != null || method.GetCustomAttribute() != null || CheckIfParentMemberIsExcluded(method)) { ResultsLogger.LogSessionItem($"Excluded method (ExcludeFromCoverage): {methodName}, class: {className}, assembly: {assemblyName}", LogVerbosityLevel.Info); if (m_ExcludedMethods == null) m_ExcludedMethods = new List(); m_ExcludedMethods.Add(method); continue; } } catch { ResultsLogger.Log(ResultID.Warning_ExcludeAttributeMethod, methodName, className, assemblyName); } if (IsGetterSetterPropertyExcluded(method, type)) { ResultsLogger.LogSessionItem($"Excluded method (ExcludeFromCoverage): {methodName}, class: {className}, assembly: {assemblyName}", LogVerbosityLevel.Info); continue; } if (classMethodStats.totalSequencePoints > 0) { List coveredSequencePoints = new List(); uint fileId = 0; int totalVisitCount = 0; CoveredSequencePoint[] classMethodSequencePointsArray = m_StatsProvider.GetSequencePointsFor(method); foreach (CoveredSequencePoint classMethodSequencePoint in classMethodSequencePointsArray) { // Skip hidden sequence points if (classMethodSequencePoint.line == 0xfeefee) // 16707566 { ResultsLogger.LogSessionItem($"Ignored sequence point - Invalid line number: {classMethodSequencePoint.line} in method: {methodName} in class: {className}", LogVerbosityLevel.Info); continue; } string filename = classMethodSequencePoint.filename; if (filesNotFound.Contains(filename) || !m_ReporterFilter.ShouldProcessFile(filename)) { ResultsLogger.LogSessionItem($"Excluded method (Path Filtering): {methodName}, class: {className}, assembly: {assemblyName}", LogVerbosityLevel.Verbose); continue; } if (!fileList.TryGetValue(filename, out fileId)) { if (!File.Exists(filename)) { filesNotFound.Add(filename); continue; } else { fileId = ++fileUID; fileList.Add(filename, fileId); } } SequencePoint coveredSequencePoint = new SequencePoint(); coveredSequencePoint.FileId = fileId; coveredSequencePoint.StartLine = (int)classMethodSequencePoint.line; coveredSequencePoint.StartColumn = (int)classMethodSequencePoint.column; coveredSequencePoint.EndLine = (int)classMethodSequencePoint.line; coveredSequencePoint.EndColumn = (int)classMethodSequencePoint.column; coveredSequencePoint.VisitCount = reportType == CoverageReportType.FullEmpty ? 0 : (int)classMethodSequencePoint.hitCount; totalVisitCount += coveredSequencePoint.VisitCount; coveredSequencePoint.Offset = (int)classMethodSequencePoint.ilOffset; if (testMethodInfo != null) { SetupTrackedMethod(coveredSequencePoint, trackedMethodUID); } coveredSequencePoints.Add(coveredSequencePoint); } bool includeMethod = true; if (reportType == CoverageReportType.CoveredMethodsOnly && totalVisitCount == 0 && !type.IsGenericType && !method.IsGenericMethod) { includeMethod = false; } else if (coveredSequencePoints.Count == 0) { includeMethod = false; } if (includeMethod) { Method coveredMethod = new Method(); coveredMethod.MetadataToken = method.MetadataToken; coveredMethod.FullName = GenerateMethodName(method); coveredMethod.FileRef = new FileRef() { UniqueId = fileId }; coveredMethod.IsConstructor = IsConstructor(method) || IsStaticConstructor(method); coveredMethod.IsStatic = method.IsStatic; coveredMethod.IsSetter = IsPropertySetter(method); coveredMethod.IsGetter = IsPropertyGetter(method); coveredMethod.SequencePoints = coveredSequencePoints.ToArray(); if (shouldGenerateAdditionalMetrics) { coveredMethod.CyclomaticComplexity = method.CalculateCyclomaticComplexity(); } ResultsLogger.LogSessionItem($"Processing method: {coveredMethod.FullName}, class: {className}, assembly: {assemblyName}", LogVerbosityLevel.Verbose); coveredMethods.Add(coveredMethod); } } } if (coveredMethods.Count > 0) { Class coveredClass = new Class(); coveredClass.FullName = GenerateTypeName(type); coveredClass.Methods = coveredMethods.ToArray(); coveredClasses.Add(coveredClass); } } } if (coveredClasses.Count != 0) { Module module = new Module(); module.ModuleName = assembly.GetName().Name; if (testMethodInfo != null) { SetupTrackedMethod(module, testMethodInfo, trackedMethodUID); trackedMethodUID++; } List coveredFileList = new List(); foreach (KeyValuePair fileEntry in fileList) { ModelFile coveredFile = new ModelFile(); coveredFile.FullPath = CoverageUtils.NormaliseFolderSeparators(fileEntry.Key); if (CommandLineManager.instance.pathReplacingSpecified) coveredFile.FullPath = CommandLineManager.instance.pathReplacing.ReplacePath(coveredFile.FullPath); coveredFile.UniqueId = fileEntry.Value; coveredFileList.Add(coveredFile); } module.Files = coveredFileList.ToArray(); module.Classes = coveredClasses.ToArray(); moduleList.Add(module); } } if (moduleList.Count > 0) { coverageSession = new CoverageSession(); coverageSession.Modules = moduleList.ToArray(); if (reportType != CoverageReportType.FullEmpty) ProcessGenericMethods(coverageSession); foreach (Module coveredModule in moduleList) { foreach (Class coveredClass in coveredModule.Classes) { foreach (Method coveredMethod in coveredClass.Methods) { UpdateMethodSummary(coveredMethod); } UpdateClassSummary(coveredClass); } UpdateModuleSummary(coveredModule); } UpdateSessionSummary(coverageSession); } ResultsLogger.LogSessionItem("Finished OpenCover Session", LogVerbosityLevel.Info); return coverageSession; } internal bool IsGetterSetterPropertyExcluded(MethodBase method, Type type) { if (IsPropertySetter(method) || IsPropertyGetter(method)) { PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); PropertyInfo property = properties.FirstOrDefault(pInfo => pInfo.GetMethod?.Name.Equals(method.Name) == true || pInfo.SetMethod?.Name.Equals(method.Name) == true); if (property?.GetCustomAttribute() != null || property?.GetCustomAttribute() != null) { return true; } } return false; } private void SetupTrackedMethod(SequencePoint sequencePoint, uint id) { TrackedMethodRef trackedMethodRef = new TrackedMethodRef(); trackedMethodRef.UniqueId = id; trackedMethodRef.VisitCount = sequencePoint.VisitCount; TrackedMethodRef[] trackedMethodRefs = new TrackedMethodRef[1]; trackedMethodRefs[0] = trackedMethodRef; sequencePoint.TrackedMethodRefs = trackedMethodRefs; } private void SetupTrackedMethod(Module module, MethodInfo testMethodInfo, uint id) { if (module.TrackedMethods == null) { TrackedMethod trackedMethod = new TrackedMethod(); trackedMethod.UniqueId = id; trackedMethod.MetadataToken = testMethodInfo.MetadataToken; trackedMethod.FullName = GenerateMethodName(testMethodInfo); trackedMethod.Strategy = "Test"; module.TrackedMethods = new TrackedMethod[1]; module.TrackedMethods[0] = trackedMethod; } } private void UpdateMethodSummary(Method coveredMethod) { int totalSequencePoints = coveredMethod.SequencePoints.Length; int numCoveredSequencePoints = coveredMethod.SequencePoints.Count(sp => sp.VisitCount > 0); coveredMethod.Visited = numCoveredSequencePoints != 0; coveredMethod.Summary.NumClasses = 0; coveredMethod.Summary.VisitedClasses = 0; coveredMethod.Summary.NumMethods = 1; coveredMethod.Summary.VisitedMethods = (coveredMethod.Visited) ? 1 : 0; coveredMethod.Summary.NumSequencePoints = totalSequencePoints; coveredMethod.Summary.VisitedSequencePoints = numCoveredSequencePoints; CalculateSummarySequenceCoverage(coveredMethod.Summary); coveredMethod.SequenceCoverage = coveredMethod.Summary.SequenceCoverage; if (m_ReporterFilter.ShouldGenerateAdditionalMetrics()) { coveredMethod.Summary.MaxCyclomaticComplexity = coveredMethod.CyclomaticComplexity; coveredMethod.Summary.MinCyclomaticComplexity = coveredMethod.CyclomaticComplexity; coveredMethod.CrapScore = CalculateCrapScore(coveredMethod.CyclomaticComplexity, coveredMethod.SequenceCoverage); coveredMethod.Summary.MaxCrapScore = coveredMethod.CrapScore; coveredMethod.Summary.MinCrapScore = coveredMethod.CrapScore; } } internal decimal CalculateCrapScore(int cyclomaticComplexity, decimal sequenceCoverage) { decimal crapScore = Math.Round((decimal)Math.Pow(cyclomaticComplexity, 2) * (decimal)Math.Pow(1.0 - (double)(sequenceCoverage / (decimal)100.0), 3.0) + cyclomaticComplexity, 2); return crapScore; } private void UpdateClassSummary(Class coveredClass) { coveredClass.Summary.NumClasses = 1; UpdateSummary(coveredClass.Summary, coveredClass.Methods); coveredClass.Summary.VisitedClasses = (coveredClass.Summary.VisitedMethods > 0) ? 1 : 0; } private void UpdateModuleSummary(Module coveredModule) { UpdateSummary(coveredModule.Summary, coveredModule.Classes); } private void UpdateSessionSummary(CoverageSession coverageSession) { UpdateSummary(coverageSession.Summary, coverageSession.Modules); } private void UpdateSummary(Summary summary, SummarySkippedEntity[] entities) { if (entities.Length > 0) { foreach (Summary entitySummary in entities.Select(entity => entity.Summary)) { summary.NumSequencePoints += entitySummary.NumSequencePoints; summary.VisitedSequencePoints += entitySummary.VisitedSequencePoints; summary.NumBranchPoints += entitySummary.NumBranchPoints; summary.VisitedBranchPoints += entitySummary.VisitedBranchPoints; summary.NumMethods += entitySummary.NumMethods; summary.VisitedMethods += entitySummary.VisitedMethods; summary.NumClasses += entitySummary.NumClasses; summary.VisitedClasses += entitySummary.VisitedClasses; } summary.MaxCyclomaticComplexity = entities.Max(entity => entity.Summary.MaxCyclomaticComplexity); summary.MinCyclomaticComplexity = entities.Min(entity => entity.Summary.MinCyclomaticComplexity); summary.MaxCrapScore = entities.Max(entity => entity.Summary.MaxCrapScore); summary.MinCrapScore = entities.Min(entity => entity.Summary.MinCrapScore); CalculateSummarySequenceCoverage(summary); CalculateSummaryBranchCoverage(summary); } } private void CalculateSummarySequenceCoverage(Summary summary) { if (summary.NumSequencePoints > 0) { summary.SequenceCoverage = decimal.Round(100.0m * (summary.VisitedSequencePoints / (decimal)summary.NumSequencePoints), 2); } else { summary.SequenceCoverage = 0.0m; } } private void CalculateSummaryBranchCoverage(Summary summary) { if (summary.NumBranchPoints > 0) { summary.BranchCoverage = decimal.Round(100.0m * (summary.VisitedBranchPoints / (decimal)summary.NumBranchPoints), 2); } else { summary.BranchCoverage = 0.0m; } } private void ProcessGenericMethods(CoverageSession coverageSession) { CoveredMethodStats[] coveredMethodStats = m_StatsProvider.GetStatsForAllCoveredMethods(); if (m_ReporterFilter.ShouldGenerateTestReferences()) { foreach (Module module in coverageSession.Modules) { foreach (Class klass in module.Classes) { // Keep methods that have some coverage klass.Methods = klass.Methods.Where(method => coveredMethodStats.Any(coveredMethod => coveredMethod.method.MetadataToken == method.MetadataToken)).ToArray(); } } } foreach (MethodBase method in coveredMethodStats.Select(coveredMethodStat => coveredMethodStat.method)) { Type declaringType = method.DeclaringType; if (!(declaringType.IsGenericType || method.IsGenericMethod)) { continue; } string assemblyName = declaringType.Assembly.GetName().Name.ToLower(); if (!m_ReporterFilter.ShouldProcessAssembly(assemblyName)) { ResultsLogger.LogSessionItem($"Excluded generic method (Assembly Filtering): {method.Name}, assembly: {assemblyName}", LogVerbosityLevel.Verbose); continue; } Module module = Array.Find(coverageSession.Modules, element => element.ModuleName.ToLower() == assemblyName); if (module != null) { string className = string.Empty; if (declaringType.IsGenericType) { Type genericTypeDefinition = declaringType.GetGenericTypeDefinition(); className = GenerateTypeName(genericTypeDefinition); } else if (method.IsGenericMethod) { className = GenerateTypeName(declaringType); } Class klass = Array.Find(module.Classes, element => element.FullName == className); if (klass != null) { Method targetMethod = Array.Find(klass.Methods, element => element.MetadataToken == method.MetadataToken); if (targetMethod != null) { ResultsLogger.LogSessionItem($"Processing generic method: {method.Name}, assembly: {assemblyName}", LogVerbosityLevel.Verbose); CoveredSequencePoint[] coveredSequencePoints = m_StatsProvider.GetSequencePointsFor(method); foreach (CoveredSequencePoint coveredSequencePoint in coveredSequencePoints) { SequencePoint targetSequencePoint = Array.Find(targetMethod.SequencePoints, element => (element.StartLine == coveredSequencePoint.line && element.Offset == coveredSequencePoint.ilOffset)); if (targetSequencePoint != null) { targetSequencePoint.VisitCount += (int)coveredSequencePoint.hitCount; if (targetSequencePoint.TrackedMethodRefs != null && targetSequencePoint.TrackedMethodRefs.Length > 0) { targetSequencePoint.TrackedMethodRefs[0].VisitCount += (int)coveredSequencePoint.hitCount; } } } } } } } } // Check if the parent member (type or method) is excluded private bool CheckIfParentMemberIsExcluded(MemberInfo member) { if (m_ExcludedMethods == null && m_ExcludedTypes == null) { return false; } Type declaringType = member.DeclaringType; while (declaringType != null) { // If parent type is excluded return true if (m_ExcludedTypes != null && m_ExcludedTypes.Any(typeName => typeName == declaringType.FullName)) { return true; } if (m_ExcludedMethods != null) { // If parent method is excluded return true foreach (var excludedMethod in m_ExcludedMethods) { if (declaringType.FullName == excludedMethod.DeclaringType.FullName && CheckIfParentMethodIsExcluded(member.Name, excludedMethod.Name)) { return true; } } } declaringType = declaringType.DeclaringType; } return false; } private bool CheckIfParentMethodIsExcluded(string methodName, string excludedMethodName) { return // Lambda method methodName.IndexOf($"<{excludedMethodName}>b__") != -1 || // yield method methodName.IndexOf($"<{excludedMethodName}>d__") != -1; } } }