diff --git a/Documentation/Changelog.md b/Documentation/Changelog.md index 56d1536e0..c9ffe9323 100644 --- a/Documentation/Changelog.md +++ b/Documentation/Changelog.md @@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Release date 2022-01-30 +### Packages +coverlet.msbuild 3.1.1 +coverlet.console 3.1.1 +coverlet.collector 3.1.1 + +### Fixed +-Fix wrong branch coverage with EnumeratorCancellation attribute [#1275](https://github.com/coverlet-coverage/coverlet/issues/1275) +-Fix negative coverage exceeding int.MaxValue [#1266](https://github.com/coverlet-coverage/coverlet/issues/1266) +-Fix summary output format for culture de-DE [#1263](https://github.com/coverlet-coverage/coverlet/issues/1263) +-Fix branch coverage issue for finally block with await [#1233](https://github.com/coverlet-coverage/coverlet/issues/1233) +-Fix threshold doesn't work when coverage empty [#1205](https://github.com/coverlet-coverage/coverlet/issues/1205) +-Fix branch coverage issue for il switch [#1177](https://github.com/coverlet-coverage/coverlet/issues/1177) +-Fix branch coverage with using statement and several awaits[#1176](https://github.com/coverlet-coverage/coverlet/issues/1176) +-Fix `CopyCoverletDataCollectorFiles` to avoid to override user dlls for `dotnet publish` scenario [#1243](https://github.com/coverlet-coverage/coverlet/pull/1243) + +### Improvements +-Improve logging in case of exception inside static ctor of NetstandardAwareAssemblyResolver [#1230](https://github.com/coverlet-coverage/coverlet/pull/1230) +-When collecting open the hitfile with read access [#1214](https://github.com/coverlet-coverage/coverlet/pull/1214) by https://github.com/JamesWTruher +-Add CompilerGenerated attribute to the tracker [#1229](https://github.com/coverlet-coverage/coverlet/pull/1229) + ## Release date 2021-07-19 ### Packages coverlet.msbuild 3.1.0 diff --git a/Documentation/KnownIssues.md b/Documentation/KnownIssues.md index 6ba92cfe5..bf1a24d95 100644 --- a/Documentation/KnownIssues.md +++ b/Documentation/KnownIssues.md @@ -150,7 +150,9 @@ or by adding the property `` to the project file NB. This **DOESN'T ALWAYS WORK**, for example in case of the shared framework https://github.com/dotnet/cli/issues/12705#issuecomment-536686785 -We can do nothing at the moment as this is a build behaviour out of our control. This issue should not happen for .NET runtime version >= 3.0 because the new default behavior is to copy all assets to the build output https://github.com/dotnet/cli/issues/12705#issuecomment-535150372 +We can do nothing at the moment as this is a build behaviour out of our control. + +For .NET runtime version >= 3.0 the new default behavior is to copy all assets to the build output (CopyLocalLockFileAssemblies=true) https://github.com/dotnet/cli/issues/12705#issuecomment-535150372, unfortunately the issue could still arise. In this case the only workaround at the moment is to *manually copy* missing dlls to the output folder: https://github.com/coverlet-coverage/coverlet/issues/560#issue-496440052 diff --git a/Documentation/ReleasePlan.md b/Documentation/ReleasePlan.md index a58f230cf..da7b5db46 100644 --- a/Documentation/ReleasePlan.md +++ b/Documentation/ReleasePlan.md @@ -23,13 +23,14 @@ We release 3 components as NuGet packages: | Package | Version | |:----------------------|:--------| -|**coverlet.msbuild** | 3.1.0 | -|**coverlet.console** | 3.1.0 | -|**coverlet.collector** | 3.1.0 | +|**coverlet.msbuild** | 3.1.1 | +|**coverlet.console** | 3.1.1 | +|**coverlet.collector** | 3.1.1 | | Release Date | coverlet.msbuild | coverlet.console | coverlet.collector| commit hash | notes | | :-----------------|:-----------------|:------------------|:------------------|:-----------------------------------------|:-------------------------------| +| 30 Jan 2022 | 3.1.1 | 3.1.1 | 3.1.1 | e4278c06faba63122a870df15a1a1b934f6bc81d | | | 19 July 2021 | 3.1.0 | 3.1.0 | 3.1.0 | 5a0ecc1e92fd754e2439dc3e4c828ff7386aa1a7 | Support for determistic build | | 21 February 2021 | 3.0.3 | 3.0.3 | 3.0.3 | adfabfd58de0aabe263e7d2080324e0b8541071e | Fix regressions | | 24 January 2021 | 3.0.2 | 3.0.2 | 3.0.2 | ed918515492193fd154b60270d440c40fa30fee9 | Fix regressions | diff --git a/eng/azure-pipelines-nightly.yml b/eng/azure-pipelines-nightly.yml index ec4d4f4b5..3535bd2bc 100644 --- a/eng/azure-pipelines-nightly.yml +++ b/eng/azure-pipelines-nightly.yml @@ -9,8 +9,8 @@ steps: - task: UseDotNet@2 inputs: - version: 5.0.101 - displayName: Install .NET Core SDK 5.0.101 + version: 5.0.401 + displayName: Install .NET Core SDK 5.0.401 - task: NuGetAuthenticate@0 displayName: Authenticate with NuGet feeds diff --git a/eng/azure-pipelines.yml b/eng/azure-pipelines.yml index 4b61e9866..67d6bc41e 100644 --- a/eng/azure-pipelines.yml +++ b/eng/azure-pipelines.yml @@ -15,7 +15,7 @@ jobs: Release: buildConfiguration: "Release" pool: - vmImage: 'windows-2019' + vmImage: 'windows-latest' steps: - template: build.yml - task: CopyFiles@2 @@ -45,7 +45,7 @@ jobs: Release: buildConfiguration: "Release" pool: - vmImage: 'macOS-10.14' + vmImage: 'macOS-latest' steps: - template: build.yml @@ -59,6 +59,6 @@ jobs: Release: buildConfiguration: "Release" pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - template: build.yml diff --git a/eng/build.yml b/eng/build.yml index e7f020cb2..fb3a5b9c9 100644 --- a/eng/build.yml +++ b/eng/build.yml @@ -6,8 +6,8 @@ steps: - task: UseDotNet@2 inputs: - version: 5.0.101 - displayName: Install .NET Core SDK 5.0.101 + version: 5.0.401 + displayName: Install .NET Core SDK 5.0.401 - script: dotnet restore displayName: Restore packages diff --git a/global.json b/global.json index fc9f7a45d..c07142494 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "5.0.101", + "version": "5.0.401", "rollForward": "latestMajor" } } diff --git a/src/coverlet.collector/build/netstandard1.0/coverlet.collector.targets b/src/coverlet.collector/build/netstandard1.0/coverlet.collector.targets index f40c7d6ee..2e4adb4c5 100644 --- a/src/coverlet.collector/build/netstandard1.0/coverlet.collector.targets +++ b/src/coverlet.collector/build/netstandard1.0/coverlet.collector.targets @@ -12,6 +12,7 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and + diff --git a/src/coverlet.console/Program.cs b/src/coverlet.console/Program.cs index 6f206674f..ae3b02d03 100644 --- a/src/coverlet.console/Program.cs +++ b/src/coverlet.console/Program.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -248,7 +249,7 @@ static int Main(string[] args) var branchPercent = summary.CalculateBranchCoverage(_module.Value).Percent; var methodPercent = summary.CalculateMethodCoverage(_module.Value).Percent; - coverageTable.AddRow(Path.GetFileNameWithoutExtension(_module.Key), $"{linePercent}%", $"{branchPercent}%", $"{methodPercent}%"); + coverageTable.AddRow(Path.GetFileNameWithoutExtension(_module.Key), $"{InvariantFormat(linePercent)}%", $"{InvariantFormat(branchPercent)}%", $"{InvariantFormat(methodPercent)}%"); } logger.LogInformation(coverageTable.ToStringAlternative()); @@ -257,8 +258,8 @@ static int Main(string[] args) coverageTable.Rows.Clear(); coverageTable.AddColumn(new[] { "", "Line", "Branch", "Method" }); - coverageTable.AddRow("Total", $"{totalLinePercent}%", $"{totalBranchPercent}%", $"{totalMethodPercent}%"); - coverageTable.AddRow("Average", $"{averageLinePercent}%", $"{averageBranchPercent}%", $"{averageMethodPercent}%"); + coverageTable.AddRow("Total", $"{InvariantFormat(totalLinePercent)}%", $"{InvariantFormat(totalBranchPercent)}%", $"{InvariantFormat(totalMethodPercent)}%"); + coverageTable.AddRow("Average", $"{InvariantFormat(averageLinePercent)}%", $"{InvariantFormat(averageBranchPercent)}%", $"{InvariantFormat(averageMethodPercent)}%"); logger.LogInformation(coverageTable.ToStringAlternative()); if (process.ExitCode > 0) @@ -314,5 +315,7 @@ static int Main(string[] args) } static string GetAssemblyVersion() => typeof(Program).Assembly.GetName().Version.ToString(); + + static string InvariantFormat(double value) => value.ToString(CultureInfo.InvariantCulture); } } diff --git a/src/coverlet.core/Coverage.cs b/src/coverlet.core/Coverage.cs index b296eba57..30714c0dd 100644 --- a/src/coverlet.core/Coverage.cs +++ b/src/coverlet.core/Coverage.cs @@ -141,7 +141,7 @@ public CoveragePrepareResult PrepareModules() } catch (Exception ex) { - _logger.LogWarning($"Unable to instrument module: {module} because : {ex.Message}"); + _logger.LogWarning($"Unable to instrument module: {module}\n{ex}"); _instrumentationHelper.RestoreOriginalModule(module, _identifier); } } @@ -408,7 +408,7 @@ private void CalculateCoverage() } var documentsList = result.Documents.Values.ToList(); - using (var fs = _fileSystem.NewFileStream(result.HitsFilePath, FileMode.Open)) + using (var fs = _fileSystem.NewFileStream(result.HitsFilePath, FileMode.Open, FileAccess.Read)) using (var br = new BinaryReader(fs)) { int hitCandidatesCount = br.ReadInt32(); @@ -421,10 +421,18 @@ private void CalculateCoverage() var document = documentsList[hitLocation.docIndex]; int hits = br.ReadInt32(); + if (hits == 0) + continue; + + hits = hits < 0 ? int.MaxValue : hits; + if (hitLocation.isBranch) { var branch = document.Branches[new BranchKey(hitLocation.start, hitLocation.end)]; branch.Hits += hits; + + if (branch.Hits < 0) + branch.Hits = int.MaxValue; } else { @@ -437,13 +445,23 @@ private void CalculateCoverage() var line = document.Lines[j]; line.Hits += hits; + + if (line.Hits < 0) + line.Hits = int.MaxValue; } } } } - _instrumentationHelper.DeleteHitsFile(result.HitsFilePath); - _logger.LogVerbose($"Hit file '{result.HitsFilePath}' deleted"); + try + { + _instrumentationHelper.DeleteHitsFile(result.HitsFilePath); + _logger.LogVerbose($"Hit file '{result.HitsFilePath}' deleted"); + } + catch (Exception ex) + { + _logger.LogWarning($"Unable to remove hit file: {result.HitsFilePath} because : {ex.Message}"); + } } } diff --git a/src/coverlet.core/CoverageResult.cs b/src/coverlet.core/CoverageResult.cs index ef29a2db4..e02e5e0f5 100644 --- a/src/coverlet.core/CoverageResult.cs +++ b/src/coverlet.core/CoverageResult.cs @@ -118,26 +118,16 @@ public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summar { case ThresholdStatistic.Minimum: { + if (!Modules.Any()) + thresholdTypeFlags = CompareThresholdValues(thresholdTypeFlagValues, thresholdTypeFlags, 0, 0, 0); + foreach (var module in Modules) { double line = summary.CalculateLineCoverage(module.Value).Percent; double branch = summary.CalculateBranchCoverage(module.Value).Percent; double method = summary.CalculateMethodCoverage(module.Value).Percent; - - if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Line, out var lineThresholdValue) && lineThresholdValue > line) - { - thresholdTypeFlags |= ThresholdTypeFlags.Line; - } - if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Branch, out var branchThresholdValue) && branchThresholdValue > branch) - { - thresholdTypeFlags |= ThresholdTypeFlags.Branch; - } - - if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Method, out var methodThresholdValue) && methodThresholdValue > method) - { - thresholdTypeFlags |= ThresholdTypeFlags.Method; - } + thresholdTypeFlags = CompareThresholdValues(thresholdTypeFlagValues, thresholdTypeFlags, line, branch, method); } } break; @@ -146,21 +136,8 @@ public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summar double line = summary.CalculateLineCoverage(Modules).AverageModulePercent; double branch = summary.CalculateBranchCoverage(Modules).AverageModulePercent; double method = summary.CalculateMethodCoverage(Modules).AverageModulePercent; - - if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Line, out var lineThresholdValue) && lineThresholdValue > line) - { - thresholdTypeFlags |= ThresholdTypeFlags.Line; - } - if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Branch, out var branchThresholdValue) && branchThresholdValue > branch) - { - thresholdTypeFlags |= ThresholdTypeFlags.Branch; - } - - if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Method, out var methodThresholdValue) && methodThresholdValue > method) - { - thresholdTypeFlags |= ThresholdTypeFlags.Method; - } + thresholdTypeFlags = CompareThresholdValues(thresholdTypeFlagValues, thresholdTypeFlags, line, branch, method); } break; case ThresholdStatistic.Total: @@ -169,25 +146,37 @@ public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summar double branch = summary.CalculateBranchCoverage(Modules).Percent; double method = summary.CalculateMethodCoverage(Modules).Percent; - if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Line, out var lineThresholdValue) && lineThresholdValue > line) - { - thresholdTypeFlags |= ThresholdTypeFlags.Line; - } - - if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Branch, out var branchThresholdValue) && branchThresholdValue > branch) - { - thresholdTypeFlags |= ThresholdTypeFlags.Branch; - } - - if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Method, out var methodThresholdValue) && methodThresholdValue > method) - { - thresholdTypeFlags |= ThresholdTypeFlags.Method; - } + thresholdTypeFlags = CompareThresholdValues(thresholdTypeFlagValues, thresholdTypeFlags, line, branch, method); } break; } return thresholdTypeFlags; } + + private static ThresholdTypeFlags CompareThresholdValues( + Dictionary thresholdTypeFlagValues, ThresholdTypeFlags thresholdTypeFlags, + double line, double branch, double method) + { + if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Line, out var lineThresholdValue) && + lineThresholdValue > line) + { + thresholdTypeFlags |= ThresholdTypeFlags.Line; + } + + if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Branch, out var branchThresholdValue) && + branchThresholdValue > branch) + { + thresholdTypeFlags |= ThresholdTypeFlags.Branch; + } + + if (thresholdTypeFlagValues.TryGetValue(ThresholdTypeFlags.Method, out var methodThresholdValue) && + methodThresholdValue > method) + { + thresholdTypeFlags |= ThresholdTypeFlags.Method; + } + + return thresholdTypeFlags; + } } } \ No newline at end of file diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs index 88ca68069..10e301730 100644 --- a/src/coverlet.core/Instrumentation/Instrumenter.cs +++ b/src/coverlet.core/Instrumentation/Instrumenter.cs @@ -123,7 +123,7 @@ public bool CanInstrument() } catch (Exception ex) { - _logger.LogWarning($"Unable to instrument module: '{_module}' because : {ex.Message}"); + _logger.LogWarning($"Unable to instrument module: '{_module}'\n{ex}"); return false; } } @@ -519,27 +519,34 @@ private void InstrumentMethod(MethodDefinition method) InstrumentIL(method); } + /// + /// The base idea is to inject an int placeholder for every sequence point. We register source+placeholder+lines(from sequence point) for final accounting. + /// Instrumentation alg(current instruction: instruction we're analyzing): + /// 1) We get all branches for the method + /// 2) We get the sequence point of every instruction of method(start line/end line) + /// 3) We check if current instruction is reachable and coverable + /// 4) For every sequence point of an instruction we put load(int hint placeholder)+call opcode above current instruction + /// 5) We patch all jump to current instruction with first injected instruction(load) + /// 6) If current instruction is a target for a branch we inject again load(int hint placeholder)+call opcode above current instruction + /// 7) We patch all jump to current instruction with first injected instruction(load) + /// private void InstrumentIL(MethodDefinition method) { method.Body.SimplifyMacros(); ILProcessor processor = method.Body.GetILProcessor(); - var index = 0; var count = processor.Body.Instructions.Count; - var branchPoints = _cecilSymbolHelper.GetBranchPoints(method); - var unreachableRanges = _reachabilityHelper.FindUnreachableIL(processor.Body.Instructions, processor.Body.ExceptionHandlers); var currentUnreachableRangeIx = 0; - for (int n = 0; n < count; n++) { - var instruction = processor.Body.Instructions[index]; - var sequencePoint = method.DebugInformation.GetSequencePoint(instruction); - var targetedBranchPoints = branchPoints.Where(p => p.EndOffset == instruction.Offset); + var currentInstruction = processor.Body.Instructions[index]; + var sequencePoint = method.DebugInformation.GetSequencePoint(currentInstruction); + var targetedBranchPoints = branchPoints.Where(p => p.EndOffset == currentInstruction.Offset); // make sure we're looking at the correct unreachable range (if any) - var instrOffset = instruction.Offset; + var instrOffset = currentInstruction.Offset; while (currentUnreachableRangeIx < unreachableRanges.Length && instrOffset > unreachableRanges[currentUnreachableRangeIx].EndOffset) { currentUnreachableRangeIx++; @@ -554,7 +561,7 @@ private void InstrumentIL(MethodDefinition method) } // Check is both reachable, _and_ coverable - if (isUnreachable || _cecilSymbolHelper.SkipNotCoverableInstruction(method, instruction)) + if (isUnreachable || _cecilSymbolHelper.SkipNotCoverableInstruction(method, currentInstruction)) { index++; continue; @@ -562,18 +569,18 @@ private void InstrumentIL(MethodDefinition method) if (sequencePoint != null && !sequencePoint.IsHidden) { - if (_cecilSymbolHelper.SkipInlineAssignedAutoProperty(_parameters.SkipAutoProps, method, instruction)) + if (_cecilSymbolHelper.SkipInlineAssignedAutoProperty(_parameters.SkipAutoProps, method, currentInstruction)) { index++; continue; } - var target = AddInstrumentationCode(method, processor, instruction, sequencePoint); - foreach (var _instruction in processor.Body.Instructions) - ReplaceInstructionTarget(_instruction, instruction, target); + var firstInjectedInstrumentedOpCode = AddInstrumentationCode(method, processor, currentInstruction, sequencePoint); + foreach (var bodyInstruction in processor.Body.Instructions) + ReplaceInstructionTarget(bodyInstruction, currentInstruction, firstInjectedInstrumentedOpCode); foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) - ReplaceExceptionHandlerBoundary(handler, instruction, target); + ReplaceExceptionHandlerBoundary(handler, currentInstruction, firstInjectedInstrumentedOpCode); index += 2; } @@ -589,12 +596,12 @@ private void InstrumentIL(MethodDefinition method) if (branchTarget.StartLine == -1 || branchTarget.Document == null) continue; - var target = AddInstrumentationCode(method, processor, instruction, branchTarget); - foreach (var _instruction in processor.Body.Instructions) - ReplaceInstructionTarget(_instruction, instruction, target); + var firstInjectedInstrumentedOpCode = AddInstrumentationCode(method, processor, currentInstruction, branchTarget); + foreach (var bodyInstruction in processor.Body.Instructions) + ReplaceInstructionTarget(bodyInstruction, currentInstruction, firstInjectedInstrumentedOpCode); foreach (ExceptionHandler handler in processor.Body.ExceptionHandlers) - ReplaceExceptionHandlerBoundary(handler, instruction, target); + ReplaceExceptionHandlerBoundary(handler, currentInstruction, firstInjectedInstrumentedOpCode); index += 2; } @@ -704,20 +711,20 @@ private Instruction AddInstrumentationInstructions(MethodDefinition method, ILPr private static void ReplaceInstructionTarget(Instruction instruction, Instruction oldTarget, Instruction newTarget) { - if (instruction.Operand is Instruction _instruction) + if (instruction.Operand is Instruction operandInstruction) { - if (_instruction == oldTarget) + if (operandInstruction == oldTarget) { instruction.Operand = newTarget; return; } } - else if (instruction.Operand is Instruction[] _instructions) + else if (instruction.Operand is Instruction[] operandInstructions) { - for (int i = 0; i < _instructions.Length; i++) + for (int i = 0; i < operandInstructions.Length; i++) { - if (_instructions[i] == oldTarget) - _instructions[i] = newTarget; + if (operandInstructions[i] == oldTarget) + operandInstructions[i] = newTarget; } } } diff --git a/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs index 251dcac24..653178782 100644 --- a/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs +++ b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; namespace Coverlet.Core.Instrumentation @@ -15,6 +16,7 @@ namespace Coverlet.Core.Instrumentation /// As this type is going to be customized for each instrumented module it doesn't follow typical practices /// regarding visibility of members, etc. /// + [CompilerGenerated] [ExcludeFromCodeCoverage] internal static class ModuleTrackerTemplate { diff --git a/src/coverlet.core/Symbols/CecilSymbolHelper.cs b/src/coverlet.core/Symbols/CecilSymbolHelper.cs index 25909720c..b6727de46 100644 --- a/src/coverlet.core/Symbols/CecilSymbolHelper.cs +++ b/src/coverlet.core/Symbols/CecilSymbolHelper.cs @@ -124,8 +124,8 @@ private static bool SkipMoveNextPrologueBranches(Instruction instruction) If method is a generated MoveNext we'll skip first branches (could be a switch or a series of branches) that check state machine value to jump to correct state (for instance after a true async call) Check if it's a Cond_Branch on state machine current value int num = <>1__state; - We are on branch OpCode so we need to go back by max 2 operation to reach ldloc.0 the load of "num" - Max 2 because we handle following patterns + We are on branch OpCode so we need to go back by max 3 operation to reach ldloc.0 the load of "num" + Max 3 because we handle following patterns Swich @@ -168,7 +168,7 @@ so we know that current branch are checking that field and we're not interested */ Instruction current = instruction.Previous; - for (int instructionBefore = 2; instructionBefore > 0 && current.Previous != null; current = current.Previous, instructionBefore--) + for (int instructionBefore = 3; instructionBefore > 0 && current.Previous != null; current = current.Previous, instructionBefore--) { if ( (current.OpCode == OpCodes.Ldloc && current.Operand is VariableDefinition vo && vo.Index == 0) || @@ -543,9 +543,9 @@ static bool CheckIfExceptionThrown(List instructions, Instruction i instructions[i].Operand is FieldDefinition field && IsCompilerGenerated(field) && field.FieldType.FullName == "System.Object") { - // We expect the call to GetResult() to be no more than three + // We expect the call to GetResult() to be no more than four // instructions before the loading of the field's value. - int minCallIndex = Math.Max(0, i - 3); + int minCallIndex = Math.Max(0, i - 4); for (int j = i - 1; j >= minCallIndex; --j) { @@ -855,7 +855,6 @@ instructions[i].Operand is FieldDefinition field && return false; } - static bool DisposeCheck(List instructions, Instruction instruction, int currentIndex) { // Within the compiler-generated async iterator, there are at least a @@ -891,6 +890,40 @@ static bool DisposeCheck(List instructions, Instruction instruction } } + private bool SkipGeneratedBranchesForEnumeratorCancellationAttribute(List instructions, Instruction instruction) + { + // For async-enumerable methods an additional cancellation token despite the default one can be passed. + // The EnumeratorCancellation attribute marks the parameter whose value is received by GetAsyncEnumerator(CancellationToken). + // Therefore the compiler generates the field x__combinedTokens and generates some additional branch points. + // + // IL_0118: ldarg.0 + // IL_0119: ldfld class [System.Runtime]System.Threading.CancellationTokenSource Issue1275.AwaitForeachReproduction/'d__1'::'<>x__combinedTokens' + // IL_011E: brfalse.s IL_0133 + // + // We'll eliminate these wherever they appear. It's reasonable to just look for a "brfalse" or "brfalse.s" instruction, preceded + // immediately by "ldfld" of the compiler-generated "<>x__combinedTokens" field. + + int branchIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer()); + + if (instruction.OpCode != OpCodes.Brfalse && + instruction.OpCode != OpCodes.Brfalse_S) + { + return false; + } + + if (branchIndex >= 2 && + instructions[branchIndex - 1].OpCode == OpCodes.Ldfld && + instructions[branchIndex - 1].Operand is FieldDefinition field && + field.FieldType.FullName.Equals("System.Threading.CancellationTokenSource") && + field.FullName.EndsWith("x__combinedTokens") && + (instructions[branchIndex - 2].OpCode == OpCodes.Ldarg || + instructions[branchIndex - 2].OpCode == OpCodes.Ldarg_0)) + { + return true; + } + return false; + } + // https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/Expression%20Breakpoints.md private static bool SkipExpressionBreakpointsBranches(Instruction instruction) => instruction.Previous is not null && instruction.Previous.OpCode == OpCodes.Ldc_I4 && instruction.Previous.Operand is int operandValue && operandValue == 1 && @@ -973,6 +1006,11 @@ public IReadOnlyList GetBranchPoints(MethodDefinition methodDefinit } } + if (SkipGeneratedBranchesForEnumeratorCancellationAttribute(instructions, instruction)) + { + continue; + } + if (SkipBranchGeneratedExceptionFilter(instruction, methodDefinition)) { continue; @@ -1446,4 +1484,4 @@ public int Compare(Instruction x, Instruction y) } } } -} \ No newline at end of file +} diff --git a/src/coverlet.core/coverlet.core.csproj b/src/coverlet.core/coverlet.core.csproj index 1e8cc43d7..884744854 100644 --- a/src/coverlet.core/coverlet.core.csproj +++ b/src/coverlet.core/coverlet.core.csproj @@ -3,7 +3,7 @@ Library netstandard2.0 - 5.7.0 + 5.7.1 false diff --git a/src/coverlet.msbuild.tasks/CoverageResultTask.cs b/src/coverlet.msbuild.tasks/CoverageResultTask.cs index 3c1d72a74..ebc17a0e4 100644 --- a/src/coverlet.msbuild.tasks/CoverageResultTask.cs +++ b/src/coverlet.msbuild.tasks/CoverageResultTask.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -206,7 +207,7 @@ public override bool Execute() var branchPercent = summary.CalculateBranchCoverage(module.Value).Percent; var methodPercent = summary.CalculateMethodCoverage(module.Value).Percent; - coverageTable.AddRow(Path.GetFileNameWithoutExtension(module.Key), $"{linePercent}%", $"{branchPercent}%", $"{methodPercent}%"); + coverageTable.AddRow(Path.GetFileNameWithoutExtension(module.Key), $"{InvariantFormat(linePercent)}%", $"{InvariantFormat(branchPercent)}%", $"{InvariantFormat(methodPercent)}%"); } Console.WriteLine(); @@ -216,8 +217,8 @@ public override bool Execute() coverageTable.Rows.Clear(); coverageTable.AddColumn(new[] { "", "Line", "Branch", "Method" }); - coverageTable.AddRow("Total", $"{totalLinePercent}%", $"{totalBranchPercent}%", $"{totalMethodPercent}%"); - coverageTable.AddRow("Average", $"{averageLinePercent}%", $"{averageBranchPercent}%", $"{averageMethodPercent}%"); + coverageTable.AddRow("Total", $"{InvariantFormat(totalLinePercent)}%", $"{InvariantFormat(totalBranchPercent)}%", $"{InvariantFormat(totalMethodPercent)}%"); + coverageTable.AddRow("Average", $"{InvariantFormat(averageLinePercent)}%", $"{InvariantFormat(averageBranchPercent)}%", $"{InvariantFormat(averageMethodPercent)}%"); Console.WriteLine(coverageTable.ToStringAlternative()); @@ -254,5 +255,7 @@ public override bool Execute() return true; } + + private static string InvariantFormat(double value) => value.ToString(CultureInfo.InvariantCulture); } } diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs index 56b08fcdc..40d733b7f 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Coverlet.Core.Samples.Tests; @@ -125,5 +127,91 @@ public void AsyncAwait_Issue_669_2() File.Delete(path); } } + + [Fact] + public void AsyncAwait_Issue_1177() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => + { + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + ((Task)instance.Test()).ConfigureAwait(false).GetAwaiter().GetResult(); + return Task.CompletedTask; + }, + persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + var document = TestInstrumentationHelper.GetCoverageResult(path).Document("Instrumentation.AsyncAwait.cs"); + document.AssertLinesCovered(BuildConfiguration.Debug, (133, 1), (134, 1), (135, 1), (136, 1), (137, 1)); + Assert.DoesNotContain(document.Branches, x => x.Key.Line == 134); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void AsyncAwait_Issue_1233() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => + { + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + ((Task)instance.Test()).ConfigureAwait(false).GetAwaiter().GetResult(); + return Task.CompletedTask; + }, + persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + var document = TestInstrumentationHelper.GetCoverageResult(path).Document("Instrumentation.AsyncAwait.cs"); + document.AssertLinesCovered(BuildConfiguration.Debug, (150, 1)); + Assert.DoesNotContain(document.Branches, x => x.Key.Line == 150); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void AsyncAwait_Issue_1275() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => + { + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(instance => + { + var cts = new CancellationTokenSource(); + ((Task)instance.Execute(cts.Token)).ConfigureAwait(false).GetAwaiter().GetResult(); + return Task.CompletedTask; + }, + persistPrepareResultToFile: pathSerialize[0]); + + return 0; + }, new string[] { path }); + + var document = TestInstrumentationHelper.GetCoverageResult(path).Document("Instrumentation.AsyncAwait.cs"); + document.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 170, 176); + document.AssertBranchesCovered(BuildConfiguration.Debug, (171, 0, 1), (171, 1, 1)); + Assert.DoesNotContain(document.Branches, x => x.Key.Line == 176); + } + finally + { + File.Delete(path); + } + } } } \ No newline at end of file diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.IntegerOverflow.cs b/test/coverlet.core.tests/Coverage/CoverageTests.IntegerOverflow.cs new file mode 100644 index 000000000..5b9121f81 --- /dev/null +++ b/test/coverlet.core.tests/Coverage/CoverageTests.IntegerOverflow.cs @@ -0,0 +1,65 @@ +using System.IO; +using Coverlet.Core.Abstractions; +using Coverlet.Core.Instrumentation; +using Moq; +using Xunit; + +namespace Coverlet.Core.Tests +{ + public partial class CoverageTests + { + [Fact] + public void CoverageResult_NegativeLineCoverage_TranslatedToMaxValueOfInt32() + { + InstrumenterResult instrumenterResult = new InstrumenterResult + { + HitsFilePath = "HitsFilePath", + SourceLink = "SourceLink", + ModulePath = "ModulePath" + }; + + instrumenterResult.HitCandidates.Add(new HitCandidate(false, 0, 1, 1)); + + var document = new Document + { + Index = 0, + Path = "Path0" + }; + + document.Lines.Add(1, new Line + { + Class = "Class0", + Hits = 0, + Method = "Method0", + Number = 1 + }); + + instrumenterResult.Documents.Add("document", document); + + CoveragePrepareResult coveragePrepareResult = new CoveragePrepareResult + { + UseSourceLink = true, + Results = new[] {instrumenterResult}, + Parameters = new CoverageParameters() + }; + + Stream memoryStream = new MemoryStream(); + BinaryWriter binaryWriter = new BinaryWriter(memoryStream); + binaryWriter.Write(1); + binaryWriter.Write(-1); + memoryStream.Position = 0; + + var fileSystemMock = new Mock(); + fileSystemMock.Setup(x => x.Exists(It.IsAny())).Returns(true); + fileSystemMock.Setup(x => x.NewFileStream(It.IsAny(), FileMode.Open, FileAccess.Read)) + .Returns(memoryStream); + + var coverage = new Coverage(coveragePrepareResult, new Mock().Object, new Mock().Object, + fileSystemMock.Object, new Mock().Object); + + var coverageResult = coverage.GetCoverageResult(); + coverageResult.Document("document").AssertLinesCovered(BuildConfiguration.Debug, (1, int.MaxValue)); + + } + } +} diff --git a/test/coverlet.core.tests/CoverageResultTests.cs b/test/coverlet.core.tests/CoverageResultTests.cs index c02d02ce4..d391ac45c 100644 --- a/test/coverlet.core.tests/CoverageResultTests.cs +++ b/test/coverlet.core.tests/CoverageResultTests.cs @@ -148,5 +148,26 @@ public void TestGetThresholdTypesBelowThresholdAllFail() ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); Assert.Equal(thresholdTypeFlags, resThresholdTypeFlags); } + + [Fact] + public void TestGetThresholdTypesBelowThresholdWhenNoModuleInstrumented() + { + CoverageResult result = new CoverageResult(); + result.Modules = new Modules(); + + CoverageSummary summary = new CoverageSummary(); + Dictionary thresholdTypeFlagValues = new Dictionary() + { + { ThresholdTypeFlags.Line, 80 }, + { ThresholdTypeFlags.Method, 80 }, + { ThresholdTypeFlags.Branch, 80 }, + }; + + ThresholdTypeFlags thresholdTypeFlags = ThresholdTypeFlags.Line | ThresholdTypeFlags.Branch | ThresholdTypeFlags.Method; + ThresholdStatistic thresholdStatic = ThresholdStatistic.Minimum; + + ThresholdTypeFlags resThresholdTypeFlags = result.GetThresholdTypesBelowThreshold(summary, thresholdTypeFlagValues, thresholdStatic); + Assert.Equal(thresholdTypeFlags, resThresholdTypeFlags); + } } } \ No newline at end of file diff --git a/test/coverlet.core.tests/Samples/Instrumentation.AsyncAwait.cs b/test/coverlet.core.tests/Samples/Instrumentation.AsyncAwait.cs index 3270a77b6..31361c962 100644 --- a/test/coverlet.core.tests/Samples/Instrumentation.AsyncAwait.cs +++ b/test/coverlet.core.tests/Samples/Instrumentation.AsyncAwait.cs @@ -125,4 +125,54 @@ public interface IService Task Process(string cat); } } + + public class Issue_1177 + { + async public Task Test() + { + await Task.CompletedTask; + using var _ = new System.IO.MemoryStream(); + await Task.CompletedTask; + await Task.CompletedTask; + await Task.CompletedTask; + } + } + + public class Issue_1233 + { + async public Task Test() + { + try + { + } + finally + { + await Task.CompletedTask; + } + } + } + + public class Issue_1275 + { + public async Task Execute(System.Threading.CancellationToken token) + { + int sum = 0; + + await foreach (int result in AsyncEnumerable(token)) + { + sum += result; + } + + return sum; + } + + async System.Collections.Generic.IAsyncEnumerable AsyncEnumerable([System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken) + { + for (int i = 0; i < 1; i++) + { + await Task.Delay(1, cancellationToken); + yield return i; + } + } + } } diff --git a/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj b/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj index 7a7d09a09..03637dad4 100644 --- a/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj +++ b/test/coverlet.tests.projectsample.fsharp/coverlet.tests.projectsample.fsharp.fsproj @@ -3,6 +3,7 @@ net5.0 true + false false diff --git a/version.json b/version.json index 21f6e5e72..9591bc471 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.1.1-preview.{height}", + "version": "3.1.2-preview.{height}", "publicReleaseRefSpec": [ "^refs/heads/master$" ],