Skip to content

Skip assigned auto properties and records #1159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Changes from all 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
@@ -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
@@ -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);
55 changes: 55 additions & 0 deletions src/coverlet.core/Symbols/CecilSymbolHelper.cs
Original file line number Diff line number Diff line change
@@ -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.EndsWith("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)
110 changes: 102 additions & 8 deletions test/coverlet.core.tests/Coverage/CoverageTests.AutoProps.cs
Original file line number Diff line number Diff line change
@@ -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]));
@@ -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
21 changes: 21 additions & 0 deletions test/coverlet.core.tests/Samples/Instrumentation.AutoProps.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}