Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/coverlet.core/Abstractions/ICecilSymbolHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ internal interface ICecilSymbolHelper
{
IReadOnlyList<BranchPoint> GetBranchPoints(MethodDefinition methodDefinition);
bool SkipNotCoverableInstruction(MethodDefinition methodDefinition, Instruction instruction);
bool SkipInlineAssignedAutoProperty(bool skipAutoProps, MethodDefinition methodDefinition, Instruction instruction);
}
}
6 changes: 6 additions & 0 deletions src/coverlet.core/Instrumentation/Instrumenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,12 @@ private void InstrumentIL(MethodDefinition method)

if (sequencePoint != null && !sequencePoint.IsHidden)
{
if (_cecilSymbolHelper.SkipInlineAssignedAutoProperty(_parameters.SkipAutoProps, method, instruction))
{
index++;
continue;
}

var target = AddInstrumentationCode(method, processor, instruction, sequencePoint);
foreach (var _instruction in processor.Body.Instructions)
ReplaceInstructionTarget(_instruction, instruction, target);
Expand Down
55 changes: 55 additions & 0 deletions src/coverlet.core/Symbols/CecilSymbolHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,61 @@ private bool SkipExpressionBreakpointsSequences(MethodDefinition methodDefinitio
return false;
}

public bool SkipInlineAssignedAutoProperty(bool skipAutoProps, MethodDefinition methodDefinition, Instruction instruction)
{
if (!skipAutoProps || !methodDefinition.IsConstructor) return false;

return SkipGeneratedBackingFieldAssignment(methodDefinition, instruction) ||
SkipDefaultInitializationSystemObject(instruction);
}

private static bool SkipGeneratedBackingFieldAssignment(MethodDefinition methodDefinition, Instruction instruction)
{
/*
For inline initialization of properties the compiler generates a field that is set in the constructor of the class.
To skip this we search for compiler generated fields that are set in the constructor.

.field private string '<SurName>k__BackingField'
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
)

.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
IL_0000: ldarg.0
IL_0001: ldsfld string[System.Runtime] System.String::Empty
IL_0006: stfld string TestRepro.ClassWithPropertyInit::'<SurName>k__BackingField'
...
}
...
*/
var autogeneratedBackingFields = methodDefinition.DeclaringType.Fields.Where(x =>
x.CustomAttributes.Any(ca => ca.AttributeType.FullName.Equals(typeof(CompilerGeneratedAttribute).FullName)) &&
x.FullName.Contains("k__BackingField"));

return instruction.OpCode == OpCodes.Ldarg &&
instruction.Next?.Next?.OpCode == OpCodes.Stfld &&
instruction.Next?.Next?.Operand is FieldReference fr &&
autogeneratedBackingFields.Select(x => x.FullName).Contains(fr.FullName);
}

private static bool SkipDefaultInitializationSystemObject(Instruction instruction)
{
/*
A type always has a constructor with a default instantiation of System.Object. For record types these
instructions can have a own sequence point. This means that even the default constructor would be instrumented.
To skip this we search for call instructions with a method reference that declares System.Object.

IL_0000: ldarg.0
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: ret
*/
return instruction.OpCode == OpCodes.Ldarg &&
instruction.Next?.OpCode == OpCodes.Call &&
instruction.Next?.Operand is MethodReference mr && mr.DeclaringType.FullName.Equals(typeof(System.Object).FullName);
}

private static bool SkipBranchGeneratedExceptionFilter(Instruction branchInstruction, MethodDefinition methodDefinition)
{
if (!methodDefinition.Body.HasExceptionHandlers)
Expand Down
110 changes: 102 additions & 8 deletions test/coverlet.core.tests/Coverage/CoverageTests.AutoProps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public void SkipAutoProps(bool skipAutoProps)
{
instance.AutoPropsNonInit = 10;
instance.AutoPropsInit = 20;
int readVal = instance.AutoPropsNonInit;
readVal = instance.AutoPropsInit;
int readValue = instance.AutoPropsNonInit;
readValue = instance.AutoPropsInit;
return Task.CompletedTask;
},
persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1]));
Expand All @@ -33,16 +33,110 @@ public void SkipAutoProps(bool skipAutoProps)
if (skipAutoProps)
{
TestInstrumentationHelper.GetCoverageResult(path)
.Document("Instrumentation.AutoProps.cs")
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 12, 12)
.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 7, 11)
.AssertLinesCovered(BuildConfiguration.Debug, (13, 1));
.Document("Instrumentation.AutoProps.cs")
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 12, 13)
.AssertNonInstrumentedLines(BuildConfiguration.Release, 12, 13)
.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 9, 11)
.AssertLinesCovered(BuildConfiguration.Debug, (7, 1))
.AssertLinesCovered(BuildConfiguration.Release, (10, 1));
}
else
{
TestInstrumentationHelper.GetCoverageResult(path)
.Document("Instrumentation.AutoProps.cs")
.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 7, 13);
.Document("Instrumentation.AutoProps.cs")
.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 7, 13)
.AssertLinesCoveredFromTo(BuildConfiguration.Release, 10, 10)
.AssertLinesCoveredFromTo(BuildConfiguration.Release, 12, 13);
}
}
finally
{
File.Delete(path);
}
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void SkipAutoPropsInRecords(bool skipAutoProps)
{
string path = Path.GetTempFileName();
try
{
FunctionExecutor.Run(async (string[] parameters) =>
{
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<RecordWithPropertyInit>(instance =>
{
instance.RecordAutoPropsNonInit = string.Empty;
instance.RecordAutoPropsInit = string.Empty;
string readValue = instance.RecordAutoPropsInit;
readValue = instance.RecordAutoPropsNonInit;
return Task.CompletedTask;
},
persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1]));

return 0;
}, new string[] { path, skipAutoProps.ToString() });

if (skipAutoProps)
{
TestInstrumentationHelper.GetCoverageResult(path).GenerateReport(show: true)
.Document("Instrumentation.AutoProps.cs")
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 23, 24)
.AssertNonInstrumentedLines(BuildConfiguration.Release, 23, 24)
.AssertLinesCovered(BuildConfiguration.Debug, (18, 1), (20, 1), (21, 1), (22, 1))
.AssertLinesCovered(BuildConfiguration.Release, (21, 1));
}
else
{
TestInstrumentationHelper.GetCoverageResult(path)
.Document("Instrumentation.AutoProps.cs")
.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 18, 24)
.AssertLinesCoveredFromTo(BuildConfiguration.Release, 21, 21)
.AssertLinesCoveredFromTo(BuildConfiguration.Release, 23, 24);
}
}
finally
{
File.Delete(path);
}
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void SkipRecordWithProperties(bool skipAutoProps)
{
string path = Path.GetTempFileName();
try
{
FunctionExecutor.Run(async (string[] parameters) =>
{
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<ClassWithAutoRecordProperties>(instance =>
{
return Task.CompletedTask;
},
persistPrepareResultToFile: parameters[0], skipAutoProps: bool.Parse(parameters[1]));

return 0;
}, new string[] { path, skipAutoProps.ToString() });

if (skipAutoProps)
{
TestInstrumentationHelper.GetCoverageResult(path)
.Document("Instrumentation.AutoProps.cs")
.AssertNonInstrumentedLines(BuildConfiguration.Debug, 29, 29)
.AssertNonInstrumentedLines(BuildConfiguration.Release, 29, 29)
.AssertLinesCovered(BuildConfiguration.Debug, (32, 1), (33, 1), (34, 1))
.AssertLinesCovered(BuildConfiguration.Release, (33, 1));

}
else
{
TestInstrumentationHelper.GetCoverageResult(path)
.Document("Instrumentation.AutoProps.cs")
.AssertLinesCovered(BuildConfiguration.Debug, (29, 3), (31, 1), (32, 1), (33, 1), (34, 1))
.AssertLinesCovered(BuildConfiguration.Release, (29, 3), (31, 1), (33, 1));
}
}
finally
Expand Down
21 changes: 21 additions & 0 deletions test/coverlet.core.tests/Samples/Instrumentation.AutoProps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,25 @@ public AutoProps()
public int AutoPropsNonInit { get; set; }
public int AutoPropsInit { get; set; } = 10;
}

public record RecordWithPropertyInit
{
private int _myRecordVal = 0;
public RecordWithPropertyInit()
{
_myRecordVal = new Random().Next();
}
public string RecordAutoPropsNonInit { get; set; }
public string RecordAutoPropsInit { get; set; } = string.Empty;
}

public class ClassWithAutoRecordProperties
{
record AutoRecordWithProperties(string Prop1, string Prop2);

public ClassWithAutoRecordProperties()
{
var record = new AutoRecordWithProperties(string.Empty, string.Empty);
}
}
}