diff --git a/Documentation/MSBuildIntegration.md b/Documentation/MSBuildIntegration.md index 6cee59fa4..84a07da62 100644 --- a/Documentation/MSBuildIntegration.md +++ b/Documentation/MSBuildIntegration.md @@ -132,6 +132,17 @@ The following command will compare the threshold value with the overall total co dotnet test /p:CollectCoverage=true /p:Threshold=80 /p:ThresholdType=line /p:ThresholdStat=total ``` +You can also specify what action to take when the coverage is below the threshold value using the `/p:ThresholdAct` option. The accepted values are: + +* `fail`: which fails the build and is the default option +* `warning`: which will not stop the build and only gives a warning console message + +The following will give a warning, but allows the build to continue when the threshold value is below 80: + +```bash +dotnet test /p:CollectCoverage=true /p:Threshold=80 /p:ThresholdAct=warning +``` + ## Excluding From Coverage ### Attributes diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index 864df117f..8aaa2e659 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -36,6 +36,7 @@ static int Main(string[] args) var threshold = new Option("--threshold", "Exits with error if the coverage % is below value.") { Arity = ArgumentArity.ZeroOrOne }; var thresholdTypes = new Option>("--threshold-type", () => new List(new string[] { "line", "branch", "method" }), "Coverage type to apply the threshold to.").FromAmong("line", "branch", "method"); var thresholdStat = new Option("--threshold-stat", () => ThresholdStatistic.Minimum, "Coverage statistic used to enforce the threshold value.") { Arity = ArgumentArity.ZeroOrOne }; + var thresholdAct = new Option("--threshold-act", () => ThresholdAction.Fail, "The action to take when coverage is below the threshold value. Defaults to failing the build.") { Arity = ArgumentArity.ZeroOrOne }; var excludeFilters = new Option("--exclude", "Filter expressions to exclude specific modules and types.") { Arity = ArgumentArity.ZeroOrMore, AllowMultipleArgumentsPerToken = true }; var includeFilters = new Option("--include", "Filter expressions to include only specific modules and types.") { Arity = ArgumentArity.ZeroOrMore, AllowMultipleArgumentsPerToken = true }; var excludedSourceFiles = new Option("--exclude-by-file", "Glob patterns specifying source files to exclude.") { Arity = ArgumentArity.ZeroOrMore, AllowMultipleArgumentsPerToken = true }; @@ -61,6 +62,7 @@ static int Main(string[] args) threshold, thresholdTypes, thresholdStat, + thresholdAct, excludeFilters, includeFilters, excludedSourceFiles, @@ -89,6 +91,7 @@ static int Main(string[] args) string thresholdValue = context.ParseResult.GetValueForOption(threshold); List thresholdTypesValue = context.ParseResult.GetValueForOption(thresholdTypes); ThresholdStatistic thresholdStatValue = context.ParseResult.GetValueForOption(thresholdStat); + ThresholdAction thresholdActValue = context.ParseResult.GetValueForOption(thresholdAct); string[] excludeFiltersValue = context.ParseResult.GetValueForOption(excludeFilters); string[] includeFiltersValue = context.ParseResult.GetValueForOption(includeFilters); string[] excludedSourceFilesValue = context.ParseResult.GetValueForOption(excludedSourceFiles); @@ -115,6 +118,7 @@ static int Main(string[] args) thresholdValue, thresholdTypesValue, thresholdStatValue, + thresholdActValue, excludeFiltersValue, includeFiltersValue, excludedSourceFilesValue, @@ -142,6 +146,7 @@ private static Task HandleCommand(string moduleOrAppDirectory, string threshold, List thresholdTypes, ThresholdStatistic thresholdStat, + ThresholdAction thresholdAct, string[] excludeFilters, string[] includeFilters, string[] excludedSourceFiles, @@ -380,12 +385,21 @@ string sourceMappingFile { exceptionMessageBuilder.AppendLine($"The {thresholdStat.ToString().ToLower()} method coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Method]}"); } - throw new Exception(exceptionMessageBuilder.ToString()); + + switch (thresholdAct) + { + case ThresholdAction.Warning: + logger.LogWarning(exceptionMessageBuilder.ToString()); + break; + case ThresholdAction.Fail: + exitCode += (int)CommandExitCodes.CoverageBelowThreshold; + throw new Exception(exceptionMessageBuilder.ToString()); + default: + throw new ArgumentOutOfRangeException(nameof(thresholdAct), thresholdAct, "Unhandled threshold action"); + } } return Task.FromResult(exitCode); - - } catch (Win32Exception we) when (we.Source == "System.Diagnostics.Process") diff --git a/src/coverlet.core/Enums/ThresholdAction.cs b/src/coverlet.core/Enums/ThresholdAction.cs new file mode 100644 index 000000000..acf7af56a --- /dev/null +++ b/src/coverlet.core/Enums/ThresholdAction.cs @@ -0,0 +1,11 @@ +// Copyright (c) Toni Solarin-Sodara +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Coverlet.Core.Enums +{ + internal enum ThresholdAction + { + Fail, + Warning + } +} diff --git a/src/coverlet.msbuild.tasks/CoverageResultTask.cs b/src/coverlet.msbuild.tasks/CoverageResultTask.cs index 87ba40c7d..305dfd163 100644 --- a/src/coverlet.msbuild.tasks/CoverageResultTask.cs +++ b/src/coverlet.msbuild.tasks/CoverageResultTask.cs @@ -37,6 +37,9 @@ public class CoverageResultTask : BaseTask [Required] public string ThresholdStat { get; set; } + [Required] + public string ThresholdAct { get; set; } + [Required] public ITaskItem InstrumenterState { get; set; } @@ -190,6 +193,16 @@ public override bool Execute() thresholdStat = ThresholdStatistic.Total; } + ThresholdAction thresholdAct = ThresholdAction.Fail; + if (ThresholdAct.Equals("fail", StringComparison.OrdinalIgnoreCase)) + { + thresholdAct = ThresholdAction.Fail; + } + else if (ThresholdAct.Equals("warning", StringComparison.OrdinalIgnoreCase)) + { + thresholdAct = ThresholdAction.Warning; + } + var coverageTable = new ConsoleTable("Module", "Line", "Branch", "Method"); var summary = new CoverageSummary(); @@ -248,7 +261,16 @@ public override bool Execute() $"The {thresholdStat.ToString().ToLower()} method coverage is below the specified {thresholdTypeFlagValues[ThresholdTypeFlags.Method]}"); } - throw new Exception(exceptionMessageBuilder.ToString()); + switch (thresholdAct) + { + case ThresholdAction.Warning: + _logger.LogWarning(exceptionMessageBuilder.ToString()); + break; + case ThresholdAction.Fail: + throw new Exception(exceptionMessageBuilder.ToString()); + default: + throw new ArgumentOutOfRangeException(nameof(thresholdAct), thresholdAct, "Unhandled threshold action"); + } } } catch (Exception ex) diff --git a/src/coverlet.msbuild.tasks/coverlet.msbuild.props b/src/coverlet.msbuild.tasks/coverlet.msbuild.props index 9403e7702..08a789aa5 100644 --- a/src/coverlet.msbuild.tasks/coverlet.msbuild.props +++ b/src/coverlet.msbuild.tasks/coverlet.msbuild.props @@ -16,6 +16,7 @@ 0 line,branch,method minimum + fail diff --git a/src/coverlet.msbuild.tasks/coverlet.msbuild.targets b/src/coverlet.msbuild.tasks/coverlet.msbuild.targets index e8bbfac20..9735fe938 100644 --- a/src/coverlet.msbuild.tasks/coverlet.msbuild.targets +++ b/src/coverlet.msbuild.tasks/coverlet.msbuild.targets @@ -75,6 +75,7 @@ Threshold="$(Threshold)" ThresholdType="$(ThresholdType)" ThresholdStat="$(ThresholdStat)" + ThresholdAct="$(ThresholdAct)" InstrumenterState="$(InstrumenterState)" CoverletMultiTargetFrameworksCurrentTFM="$(_coverletMultiTargetFrameworksCurrentTFM)"> diff --git a/test/coverlet.msbuild.tasks.tests/CoverageResultTaskTests.cs b/test/coverlet.msbuild.tasks.tests/CoverageResultTaskTests.cs index 547fcdbfb..93643948c 100644 --- a/test/coverlet.msbuild.tasks.tests/CoverageResultTaskTests.cs +++ b/test/coverlet.msbuild.tasks.tests/CoverageResultTaskTests.cs @@ -98,6 +98,7 @@ public void Execute_StateUnderTest_WithInstrumentationState_Fake() Threshold = "50", ThresholdType = "total", ThresholdStat = "total", + ThresholdAct = "fail", InstrumenterState = InstrumenterState }; coverageResultTask.BuildEngine = _buildEngine.Object;