diff --git a/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs b/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs index 3785b6bb..61799fc8 100644 --- a/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs +++ b/FineCodeCoverage/Core/ReportGenerator/ReportGeneratorUtil.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using FineCodeCoverage.Core.Utilities; +using FineCodeCoverage.Options; using Fizzler.Systems.HtmlAgilityPack; using HtmlAgilityPack; using Newtonsoft.Json; @@ -38,7 +39,8 @@ internal partial class ReportGeneratorUtil: IReportGeneratorUtil private readonly IToolFolder toolFolder; private readonly IToolZipProvider toolZipProvider; private readonly IFileUtil fileUtil; - private const string zipPrefix = "reportGenerator"; + private readonly IAppOptionsProvider appOptionsProvider; + private const string zipPrefix = "reportGenerator"; private const string zipDirectoryName = "reportGenerator"; public string ReportGeneratorExePath { get; private set; } @@ -50,10 +52,12 @@ public ReportGeneratorUtil( ILogger logger, IToolFolder toolFolder, IToolZipProvider toolZipProvider, - IFileUtil fileUtil + IFileUtil fileUtil, + IAppOptionsProvider appOptionsProvider ) { this.fileUtil = fileUtil; + this.appOptionsProvider = appOptionsProvider; this.assemblyUtil = assemblyUtil; this.processUtil = processUtil; this.logger = logger; @@ -87,12 +91,22 @@ async Task run(string outputReportType, string inputReports) { reportTypeSettings.Add($@"""-reports:{inputReports}"""); reportTypeSettings.Add($@"""-reporttypes:Cobertura"""); + } else if (outputReportType.Equals("HtmlInline_AzurePipelines", StringComparison.OrdinalIgnoreCase)) { reportTypeSettings.Add($@"""-reports:{inputReports}"""); reportTypeSettings.Add($@"""-plugins:{typeof(FccLightReportBuilder).Assembly.Location}"""); reportTypeSettings.Add($@"""-reporttypes:{(darkMode ? FccDarkReportBuilder.REPORT_TYPE : FccLightReportBuilder.REPORT_TYPE)}"""); + var options = appOptionsProvider.Get(); + var cyclomaticThreshold = options.ThresholdForCyclomaticComplexity; + var crapScoreThreshold = options.ThresholdForCrapScore; + var nPathThreshold = options.ThresholdForNPathComplexity; + + reportTypeSettings.Add($@"""riskHotspotsAnalysisThresholds:metricThresholdForCyclomaticComplexity={cyclomaticThreshold}"""); + reportTypeSettings.Add($@"""riskHotspotsAnalysisThresholds:metricThresholdForCrapScore={crapScoreThreshold}"""); + reportTypeSettings.Add($@"""riskHotspotsAnalysisThresholds:metricThresholdForNPathComplexity={nPathThreshold}"""); + } else { @@ -407,14 +421,16 @@ public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFo { button: 'btnRiskHotspots', content: 'risk-hotspots' }, ]; + var riskHotspotsTable; + var riskHotspotsElement; var addedFileIndexToRiskHotspots = false; var addFileIndexToRiskHotspotsClassLink = function(){ if(!addedFileIndexToRiskHotspots){ addedFileIndexToRiskHotspots = true; var riskHotspotsElements = document.getElementsByTagName('risk-hotspots'); if(riskHotspotsElements.length == 1){{ - var riskHotspotsElement = riskHotspotsElements[0]; - var riskHotspotsTable = riskHotspotsElement.querySelector('table'); + riskHotspotsElement = riskHotspotsElements[0]; + riskHotspotsTable = riskHotspotsElement.querySelector('table'); if(riskHotspotsTable){ var rhBody = riskHotspotsTable.querySelector('tbody'); var rows = rhBody.rows; @@ -439,10 +455,40 @@ public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFo }} } } + + // necessary for WebBrowser + function removeElement(element){ + element.parentNode.removeChild(element); + } + + function insertAfter(newNode, existingNode) { + existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling); + } + + var noHotspotsMessage + var addNoRiskHotspotsMessageIfRequired = function(){ + if(riskHotspotsTable == null){ + noHotspotsMessage = document.createElement(""p""); + noHotspotsMessage.style.margin = ""0""; + noHotspotsMessage.innerText = ""No risk hotspots found.""; + + insertAfter(noHotspotsMessage, riskHotspotsElement); + } + } + + var removeNoRiskHotspotsMessage = function(){ + if(noHotspotsMessage){ + removeElement(noHotspotsMessage); + noHotspotsMessage = null; + } + } - var openTab = function (tabIndex) { + var openTab = function (tabIndex) { if(tabIndex==2){{ addFileIndexToRiskHotspotsClassLink(); + addNoRiskHotspotsMessageIfRequired(); + }}else{{ + removeNoRiskHotspotsMessage(); }} for (var i = 0; i < tabs.length; i++) { diff --git a/FineCodeCoverage/Options/AppOptions.cs b/FineCodeCoverage/Options/AppOptions.cs index cb617714..729a6ed5 100644 --- a/FineCodeCoverage/Options/AppOptions.cs +++ b/FineCodeCoverage/Options/AppOptions.cs @@ -13,6 +13,7 @@ internal class AppOptions : DialogPage, IAppOptions private const string coverletCategory = "Coverlet"; private const string openCoverCategory = "OpenCover"; private const string outputCategory = "Output"; + private const string reportCategory = "Report"; public AppOptions():this(false) { @@ -137,6 +138,18 @@ You can also ignore additional attributes by adding to this list (short name or [Category(outputCategory)] public string FCCSolutionOutputDirectoryName { get; set; } + [Category(reportCategory)] + [Description("When cyclomatic complexity exceeds this value for a method then the method will be present in the risk hotspots tab.")] + public int ThresholdForCyclomaticComplexity { get; set; } = 30; + + [Category(reportCategory)] + [Description("When npath complexity exceeds this value for a method then the method will be present in the risk hotspots tab. OpenCover only")] + public int ThresholdForNPathComplexity { get; set; } = 200; + + [Category(reportCategory)] + [Description("When crap score exceeds this value for a method then the method will be present in the risk hotspots tab. OpenCover only")] + public int ThresholdForCrapScore { get; set; } = 15; + [SuppressMessage("Usage", "VSTHRD010:Invoke single-threaded types on Main thread")] public override void SaveSettingsToStorage() { diff --git a/FineCodeCoverage/Options/IAppOptions.cs b/FineCodeCoverage/Options/IAppOptions.cs index a754e329..126cbd6b 100644 --- a/FineCodeCoverage/Options/IAppOptions.cs +++ b/FineCodeCoverage/Options/IAppOptions.cs @@ -18,5 +18,8 @@ public interface IAppOptions string CoverletCollectorDirectoryPath { get; } string OpenCoverCustomPath { get; } string FCCSolutionOutputDirectoryName { get; } + int ThresholdForCyclomaticComplexity { get; } + int ThresholdForNPathComplexity { get; } + int ThresholdForCrapScore { get; } } } \ No newline at end of file diff --git a/README.md b/README.md index 8dbdce63..975d16d3 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,10 @@ ExcludeByFile Glob patterns specifying source files to exclude e.g. **/Migra ExcludeByAttribute Attributes to exclude from code coverage (multiple values) IncludeTestAssembly Specifies whether to report code coverage of the test assembly +ThresholdForCyclomaticComplexity When [cyclomatic complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) exceeds this value for a method then the method will be present in the risk hotspots tab. +ThresholdForNPathComplexity When [npath complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) exceeds this value for a method then the method will be present in the risk hotspots tab. OpenCover only. +ThresholdForCrapScore When [crap score](https://testing.googleblog.com/2011/02/this-code-is-crap.html) exceeds this value for a method then the method will be present in the risk hotspots tab. OpenCover only. + RunSettingsOnly Specify false for global and project options to be used for coverlet data collector configuration elements when not specified in runsettings CoverletCollectorDirectoryPath Specify path to directory containing coverlet collector files if you need functionality that the FCC version does not provide. @@ -138,6 +142,8 @@ You can ignore a method or an entire class from code coverage by creating and ap You can also ignore additional attributes by adding to the 'ExcludeByAttributes' list (short name or full name supported) e.g. : [GeneratedCode] => Present in System.CodeDom.Compiler namespace [MyCustomExcludeFromCodeCoverage] => Any custom attribute that you may define + + ``` #### Filter Expressions